From 9ff3626065334568f6b569310edbeacda1825b2f Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 13 Jun 2017 12:29:00 -0600 Subject: [PATCH 001/129] Add documentation and cleanup code. --- README.rst | 2 +- myDevices/__main__.py | 6 ++++++ myDevices/cloud/download_speed.py | 30 ++++++++++++++++++------------ myDevices/sensors/sensors.py | 9 --------- myDevices/test/updater_test.py | 5 ++--- myDevices/utils/config.py | 1 + myDevices/utils/logger.py | 15 +-------------- 7 files changed, 29 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index 120b63c..2b9c440 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ ============= Cayenne Agent ============= -The Cayenne agent is a full featured client for the `Cayenne IoT project builder `_. It sends system information as well as sensor and actuator date and responds to actuator messages initiated from the Cayenne dashboard and mobile apps. The Cayenne agent currently supports Rasbian on the Raspberry Pi but it can be extended to support additional Linux flavors and other platforms. +The Cayenne agent is a full featured client for the `Cayenne IoT project builder `_. It sends system information as well as sensor and actuator data and responds to actuator messages initiated from the Cayenne dashboard and mobile apps. The Cayenne agent currently supports Rasbian on the Raspberry Pi but it can be extended to support additional Linux flavors and other platforms. ************ Requirements diff --git a/myDevices/__main__.py b/myDevices/__main__.py index f06a68e..340eb05 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -1,3 +1,4 @@ +"""This module is the main entry """ from myDevices.utils.config import Config from os import path, getpid, remove from myDevices.cloud.client import CloudServerClient @@ -10,6 +11,7 @@ from myDevices.os.daemon import Daemon def setMemoryLimit(rsrc, megs = 200): + """Set the memory usage limit for the agent process""" size = megs * 1048576 soft, hard = getrlimit(rsrc) setrlimit(rsrc, (size, hard)) #limit to one kilobyte @@ -25,6 +27,7 @@ def setMemoryLimit(rsrc, megs = 200): client = None pidfile = '/var/run/myDevices/cayenne.pid' def signal_handler(signal, frame): + """Handle program interrupt so the agent can exit cleanly""" if client: if signal == SIGINT: info('Program interrupt received, client exiting') @@ -79,6 +82,7 @@ def displayHelp(): exit() def writePidToFile(pidfile): + """Write the process ID to a file to prevent multiple agents from running at the same time""" if path.isfile(pidfile): info(pidfile + " already exists, exiting") with open(pidfile, 'r') as file: @@ -89,7 +93,9 @@ def writePidToFile(pidfile): pid = str(getpid()) with open(pidfile, 'w') as file: file.write(pid) + def main(argv): + """Main entry point for starting the agent client""" global pidfile configfile = None scriptfile = None diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index 96b4f5f..3090308 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -1,8 +1,10 @@ +""" +This module provides a class for testing download speed +""" from datetime import datetime, timedelta from os import path, remove from urllib import request, error from myDevices.utils.logger import exception, info, warn, error, debug -from threading import Thread from time import sleep from random import randint from socket import error as socket_error @@ -16,31 +18,36 @@ defaultDownloadRate = 24*60*60 class DownloadSpeed(): + """Class for checking download speed""" + def __init__(self, config): + """Initialize variable and start download speed test""" self.downloadSpeed = None - self.uploadSpeed = None self.testTime = None self.isRunning = False self.Start() self.config = config #add a random delay to the start of download - self.delay = randint(0,100) + self.delay = randint(0, 100) + def Start(self): - #thread = Thread(target = self.Test) - #thread.start() + """Start download speed thread""" ThreadPool.Submit(self.Test) + def Test(self): + """Run speed test""" if self.isRunning: return False self.isRunning = True sleep(1) self.TestDownload() sleep(1) - self.TestUpload() self.testTime = datetime.now() self.isRunning = False return True + def TestDownload(self): + """Test download speed by retrieving a file""" try: a = datetime.now() info('Excuting regular download test for network speed') @@ -63,22 +70,21 @@ def TestDownload(self): except: exception('TestDownload Failed') return False - def TestUpload(self): - debug('Network Speed TestUpload - Not implemented') - self.uploadSpeed = 0 + def IsDownloadTime(self): + """Return true if it is time to run a new download speed test""" if self.testTime is None: return True downloadRate = int(self.config.cloudConfig.DownloadSpeedTestRate) if 'DownloadSpeedTestRate' in self.config.cloudConfig else defaultDownloadRate - if self.testTime +timedelta(seconds=downloadRate+self.delay ) < datetime.now(): + if self.testTime + timedelta(seconds=downloadRate+self.delay ) < datetime.now(): return True return False + def getDownloadSpeed(self): + """Start a new download speed test if necessary and return the download speed""" if self.IsDownloadTime(): self.Start() return self.downloadSpeed - def getUploadSpeed(self): - return self.uploadSpeed def Test(): from myDevices.utils.config import Config diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index f2533cc..2494db0 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -497,13 +497,4 @@ def SensorCommand(self, commandType, sensorName, sensorType, driverClass, method #then refresh never comes for the specific sensor #ThreadPool.SubmitParam(self.Monitor, hashKey) return retVal - # def SensorCommandTest(self): - # # self.SensorCommand('integer', 'ledswitch', 'DigitalActuator', '', '', 1) - # # sleep(5) - # # self.SensorCommand('integer', 'motorswitch', 'DigitalActuator', '', '', 1) - # sleep(5) - # self.SensorCommand('integer', 'motorswitch', 'DigitalActuator', '', '', 0) - # sleep(5) - # self.SensorCommand('integer', 'ledswitch', 'DigitalActuator', '', '', 0) -#SensorCommandTest() diff --git a/myDevices/test/updater_test.py b/myDevices/test/updater_test.py index 65d570b..b0b3bfa 100644 --- a/myDevices/test/updater_test.py +++ b/myDevices/test/updater_test.py @@ -8,14 +8,13 @@ def setUp(self): setDebug() self.config = Config('/etc/myDevices/AppSettings.ini') self.updater = Updater(self.config) + def tearDown(self): self.updater = None + def testCheckUpdate(self): self.updater.CheckUpdate() print('After CheckUpdate') - # def testStart(self): - # self.updater.start() - # print('After Updater') def main(): unittest.main() diff --git a/myDevices/utils/config.py b/myDevices/utils/config.py index 405e54e..9d506db 100644 --- a/myDevices/utils/config.py +++ b/myDevices/utils/config.py @@ -13,6 +13,7 @@ def __init__(self, path): self.config.readfp(fp) except: pass + def set(self, section, key, value): with self.mutex: try: diff --git a/myDevices/utils/logger.py b/myDevices/utils/logger.py index c7da470..e7bd49c 100644 --- a/myDevices/utils/logger.py +++ b/myDevices/utils/logger.py @@ -18,19 +18,10 @@ LOG_FORMATTER = Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt="%Y-%m-%d %H:%M:%S") LOGGER = getLogger("myDevices") LOGGER.setLevel(WARN) -#Now setup the json logger -# JsonLogger = getLogger("myDevicesData") CONSOLE_HANDLER = StreamHandler() CONSOLE_HANDLER.setFormatter(LOG_FORMATTER) LOGGER.addHandler(CONSOLE_HANDLER) -# memoryhandler = MemoryHandler(capacity=1024*10, target=CONSOLE_HANDLER) -# LOGGER.addHandler(memoryhandler) - -# JsonLogger.addHandler(CONSOLE_HANDLER) -# JsonLogger.addHandler(memoryhandler) -# rotatingFileHandler = RotatingFileHandler(JSON_FILE_LOGGER, mode='a', maxBytes=1000000) -# JsonLogger.addHandler(rotatingFileHandler) jsonData = {} messageFrequence={} @@ -53,7 +44,7 @@ def rotator(source, dest): tar.close() remove(source) # Remove old myDevices.log backups if they are older than a week. This code can be removed - # in later versions if myDevices.log files have been replaced with cayenne.log. + # in later versions if myDevices.log files have been replaced with cayenne.log. for old_file in iglob('/var/log/myDevices/myDevices.log*'): if path.getmtime(old_file) + 604800 < time.time(): remove(old_file) @@ -83,10 +74,6 @@ def rotatorJson(): debug('rotatorJson called') lastRotate = datetime.now() ThreadPool.Submit(rotatorJsonMp) - # p = Process(target=rotatorJsonMp, args=()) - # p.start() - # p.join() - #no wait for process -> p.join() except: exception('rotatorJson exception') From 3ca687193cb5002a79f97f4282cf01669d5fc0a2 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 13 Jun 2017 18:00:02 -0600 Subject: [PATCH 002/129] Add documentation and clean up client code. --- myDevices/cloud/client.py | 608 +++++++++++++++----------------------- 1 file changed, 244 insertions(+), 364 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ffb5e21..10a7b58 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -1,3 +1,9 @@ +""" +This module contains the main agent client for connecting to the Cayenne server. The client connects +to server, retrives system info as well as sensor and actuator info and sends that data to the server. +It also responds messages from the server, to set actuator values, change system config settings, etc. +""" + from socket import SOCK_STREAM, socket, AF_INET, gethostname, SHUT_RDWR from ssl import CERT_REQUIRED, wrap_socket from json import dumps, loads @@ -6,7 +12,6 @@ from time import strftime, localtime, tzset, time, sleep from queue import Queue, Empty from enum import Enum, unique -from ctypes import CDLL, CFUNCTYPE, create_string_buffer, c_char_p, c_bool, c_int, c_void_p from myDevices.utils.config import Config import myDevices.ipgetter from myDevices.utils.logger import exception, info, warn, error, debug, logJson @@ -23,72 +28,32 @@ from myDevices.utils.history import History from select import select from hashlib import sha256 -from resource import getrusage, RUSAGE_SELF from myDevices.cloud.apiclient import CayenneApiClient + +NETWORK_SETTINGS = '/etc/myDevices/Network.ini' +APP_SETTINGS = '/etc/myDevices/AppSettings.ini' +GENERAL_SLEEP_THREAD = 0.20 + + @unique class PacketTypes(Enum): - PT_ACK= 0 - PT_START_COLLECTION= 1 - PT_STOP_COLLECTION= 2 - PT_UTILIZATION= 3 - PT_SYSTEM_INFO= 4 - PT_PROCESS_LIST= 5 - PT_DRIVE_LIST= 6 - PT_DEFRAG_ANALYSIS= 7 - PT_STARTUP_APPLICATIONS= 8 - PT_DEFRAG_DRIVE= 9 - PT_DEFRAG_COMPLETED= 10 - PT_START_RDS= 11 - PT_STOP_RDS= 12 - PT_START_SCAN= 13 - PT_CHECK_SCAN= 14 - PT_STOP_SCAN= 15 - PT_START_FIX= 16 - PT_CHECK_FIX= 17 - PT_STOP_FIX= 18 - PT_SCAN_RESPONSE= 19 - PT_FIX_RESPONSE= 20 - PT_END_SCAN= 21 - PT_DEFRAG_CHECK= 22 - PT_DEVICE_INFO_MOBILE= 23 - PT_LOCK_DESKTOP= 24 - PT_RESTART_COMPUTER= 25 - PT_SHUTDOWN_COMPUTER= 26 - PT_KILL_PROCESS= 27 - PT_CMD_NOTIFY= 28 - PT_PRINTER_INFO= 29 - PT_PRINT_TEST_PAGE= 30 - PT_ENABLE_FIREWALL= 31 - PT_WINDOWS_UPDATES= 32 - PT_LAUNCH_TASKMGR= 33 - PT_MALWARE_SCAN= 34 - PT_CANCEL_MALWARE_SCAN= 35 - PT_START_RDS_LOCAL_INIT= 36 - PT_MALWARE_GET_ITEMS= 37 - PT_MALWARE_DELETE_ITEMS= 38 - PT_MALWARE_RESTORE_ITEMS= 39 - PT_REQUEST_SCHEDULES= 40 - PT_UPDATE_SCHEDULES= 41 - PT_MALWARE_THREAT_DETECTED= 42 - PT_TOGGLE_MALWARE_SCANNER= 43 - PT_MALWARE_SCANNER_STATE= 44 - PT_AGENT_MESSAGE= 45 - PT_DRIVE_ANALYSIS= 46 - PT_FILE_TRANSFER= 47 - PT_PRINT_JOBS= 48 - PT_DISPLAY_WEB_PAGE= 49 - PT_PRODUCT_INFO= 50 - PT_UNINSTALL_AGENT= 51 - PT_ASK_RDS_CONTROL= 52 - PT_ANS_RDS_CONTROL= 53 - PT_REMOTE_CONTROL_ENDED= 54 - PT_STOP_RD_INVITE= 55 - PT_CLOSE_SALSA_CONNECTION= 56 - PT_INITIALIZED= 57 - PT_LOCATION = 58 - PT_SUPPORTED_SENSORS = 59 - PT_MACHINE_SENSORS = 60 + """Packet types used when sending/receiving messages""" + PT_ACK = 0 + PT_UTILIZATION = 3 + PT_SYSTEM_INFO = 4 + PT_PROCESS_LIST = 5 + PT_STARTUP_APPLICATIONS = 8 + PT_START_RDS = 11 + PT_STOP_RDS = 12 + PT_RESTART_COMPUTER = 25 + PT_SHUTDOWN_COMPUTER = 26 + PT_KILL_PROCESS = 27 + PT_REQUEST_SCHEDULES = 40 + PT_UPDATE_SCHEDULES = 41 + PT_AGENT_MESSAGE = 45 + PT_PRODUCT_INFO = 50 + PT_UNINSTALL_AGENT = 51 PT_ADD_SENSOR = 61 PT_REMOVE_SENSOR = 62 PT_UPDATE_SENSOR = 63 @@ -101,178 +66,136 @@ class PacketTypes(Enum): PT_DATA_CHANGED = 70 PT_HISTORY_DATA = 71 PT_HISTORY_DATA_RESPONSE = 72 - PT_DISCOVERY = 73 PT_AGENT_CONFIGURATION = 74 -NETWORK_SETTINGS='/etc/myDevices/Network.ini' -APP_SETTINGS='/etc/myDevices/AppSettings.ini' -GENERAL_SLEEP_THREAD=0.20#was 0.05 - - - -# from contextlib import contextmanager -# -# @contextmanager -#import sys -# def stdout_redirector(stream): -# old_stdout = sys.stdout -# sys.stdout = stream -# try: -# yield -# finally: -# sys.stdout = old_stdout - #with open(filepathdebug, "w+") as f: #replace filepath & filename - # with stdout_redirector(f): - #tracker.print_diff() - #countDebug=countDebug+1 - #print(str(datetime.now()) + ' Count: ' + str(countDebug)) - -# debugCount=0 -# try: -# import resource -# import gc -# gc.enable() -# from objgraph import * -# import random -# from datetime import datetime -# -# #from pympler.tracker import SummaryTracker -# #tracker = SummaryTracker() -# except Exception as e: -# error('failed to load debug modules: ' + str(e)) -debugCount=0 -def Debug(): - try: - global debugCount - debugCount = debugCount + 1 - resUsage=getrusage(RUSAGE_SELF) - size=resUsage.ru_maxrss - info("Memory usage : " + str(debugCount) + " size: " + str(size)) - info("Resouce usage info: " + str(resUsage)) - # Memory leaks display currently commented out - # show_growth() - # obj=get_leaking_objects() - # warn('Leaking objects size='+str(len(obj))) - # filepathdebug='/var/log/myDebug'+str(debugCount) - # with open(filepathdebug, "w+") as f: #replace filepath & filename - # f.write('Debug resouce iteration: ' + str(debugCount) + " size: " + str(size)) - # f.write('Leaking objects size='+str(len(obj)) + '\n') - # f.write('Leaking objects size='+str(typestats()) + '\n') - # f.write('Leaking objects'+str(obj) + '\n') - except Exception as e: - error('failed to track memory: ' + str(e)) - def GetTime(): + """Return string with the current time""" tzset() - cur=time() - val=strftime("%Y-%m-%dT%T", localtime(cur)) - timezone=strftime("%z", localtime(cur)) - hourtime=int(timezone[1:3]) - timezone=timezone[:1] + str(int(timezone[1:3]))+':'+ timezone[3:7] + cur = time() + val = strftime("%Y-%m-%dT%T", localtime(cur)) + timezone = strftime("%z", localtime(cur)) + hourtime = int(timezone[1:3]) + timezone = timezone[:1] + str(int(timezone[1:3]))+':'+ timezone[3:7] if hourtime == 0: - timezone='' + timezone = '' return val + timezone -class OSInfo(Thread): + +class OSInfo(): + """Class for getting information about the OS""" + def __init__(self): - #debug("OS Info init") + """Initialize variables with OS information""" try: - sleep(GENERAL_SLEEP_THREAD) - f = open('/etc/os-release','r') - for line in f: - splitLine = line.split('=') - if len(splitLine) < 2: - continue - key = splitLine[0].strip() - value = splitLine[1].strip().replace('"', '') - if key=='PRETTY_NAME': - self.PRETTY_NAME = value - continue - if key=='NAME': - self.NAME = value - continue - if key=='VERSION_ID': - self.VERSION_ID = value - continue - if key=='VERSION': - self.VERSION = value - continue - if key=='ID_LIKE': - self.ID_LIKE = value - continue - if key=='ID': - self.ID = value - continue - if key=='ANSI_COLOR': - self.ANSI_COLOR = value - continue - if key=='HOME_URL': - self.HOME_URL = value - continue - f.close() - except: - exception ("OSInfo Unexpected error") -#READER THREAD + with open('/etc/os-release', 'r') as os_file: + for line in os_file: + splitLine = line.split('=') + if len(splitLine) < 2: + continue + key = splitLine[0].strip() + value = splitLine[1].strip().replace('"', '') + if key == 'PRETTY_NAME': + self.PRETTY_NAME = value + continue + if key == 'NAME': + self.NAME = value + continue + if key == 'VERSION_ID': + self.VERSION_ID = value + continue + if key == 'VERSION': + self.VERSION = value + continue + if key == 'ID_LIKE': + self.ID_LIKE = value + continue + if key == 'ID': + self.ID = value + continue + if key == 'ANSI_COLOR': + self.ANSI_COLOR = value + continue + if key == 'HOME_URL': + self.HOME_URL = value + continue + except: + exception("OSInfo Unexpected error") + + class ReaderThread(Thread): + """Class for reading data from the server on a thread""" + def __init__(self, name, client): + """Initialize reader thread""" debug('ReaderThread init') Thread.__init__(self, name=name) self.cloudClient = client self.Continue = True + def run(self): - debug('ReaderThread run') - debug('ReaderThread continue?:' + str(self.Continue) ) + """Read messages from the server until the thread is stopped""" + debug('ReaderThread run, continue: ' + str(self.Continue)) while self.Continue: try: sleep(GENERAL_SLEEP_THREAD) - if self.cloudClient.connected == False: + if not self.cloudClient.connected: continue - #debug('ReaderThread - Reading message') - #self.cloudClient.mutex.acquire() - bReturned = self.cloudClient.ReadMessage() - # if bReturned: - # #debug('ReaderThread process message') - # t1 = Thread(target=self.cloudClient.ProcessMessage) - # t1.start() + self.cloudClient.ReadMessage() except: - exception ("ReaderThread Unexpected error") + exception("ReaderThread Unexpected error") return + def stop(self): + """Stop reading messages from the server""" debug('ReaderThread stop') self.Continue = False + + class ProcessorThread(Thread): + """Class for processing messages from the server on a thread""" + def __init__(self, name, client): + """Initialize processor thread""" debug('ProcessorThread init') Thread.__init__(self, name=name) self.cloudClient = client self.Continue = True + def run(self): - debug('ProcessorThread run') - debug('ProcessorThread continue?:' + str(self.Continue) ) + """Process messages from the server until the thread is stopped""" + debug('ProcessorThread run, continue: ' + str(self.Continue)) while self.Continue: try: sleep(GENERAL_SLEEP_THREAD) self.cloudClient.ProcessMessage() except: - exception ("ProcessorThread Unexpected error") + exception("ProcessorThread Unexpected error") return + def stop(self): + """Stop processing messages from the server""" debug('ProcessorThread stop') self.Continue = False -#WRITER THREAD + + class WriterThread(Thread): + """Class for sending messages to the server on a thread""" + def __init__(self, name, client): + """Initialize writer thread""" debug('WriterThread init') Thread.__init__(self, name=name) self.cloudClient = client self.Continue = True + def run(self): + """Send messages to the server until the thread is stopped""" debug('WriterThread run') while self.Continue: sleep(GENERAL_SLEEP_THREAD) try: - if self.cloudClient.connected == False: + if not self.cloudClient.connected: continue message = self.cloudClient.DequeuePacket() if not message: @@ -281,32 +204,43 @@ def run(self): del message message = None except: - exception ("WriterThread Unexpected error") + exception("WriterThread Unexpected error") return + def stop(self): + """Stop sending messages to the server""" debug('WriterThread stop') self.Continue = False -#Run function at timed intervals + class TimerThread(Thread): + """Class to run a function on a thread at timed intervals""" + def __init__(self, function, interval, initial_delay=0): + """Set function to run at intervals and start thread""" Thread.__init__(self) self.setDaemon(True) self.function = function self.interval = interval self.initial_delay = initial_delay self.start() + def run(self): + """Run function at intervals""" sleep(self.initial_delay) while True: try: self.function() sleep(self.interval + GENERAL_SLEEP_THREAD) except: - exception("TimerThread Unexpected error") - + exception("TimerThread Unexpected error") + + class CloudServerClient: + """Class to connect to the server and send and receive data""" + def __init__(self, host, port, cayenneApiHost): + """Initialize the client configuration""" self.HOST = host self.PORT = port self.CayenneApiHost = cayenneApiHost @@ -335,28 +269,20 @@ def __init__(self, host, port, cayenneApiHost): self.installDate = int(time()) self.config.set('Agent', 'InstallDate', self.installDate) self.networkConfig = Config(NETWORK_SETTINGS) - #self.defaultRDServer = self.networkConfig.get('CONFIG','RemoteDesktopServerAddress') self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.Initialize() self.CheckSubscription() self.FirstRun() - self.updater = Updater(self.config, self.OnUpdateConfig) + self.updater = Updater(self.config) self.updater.start() self.initialized = True def __del__(self): + """Delete the client""" self.Destroy() - def OnUpdateConfig(self): - pass - # info('Requesting PT_AGENT_CONFIGURATION ') - # data = {} - # data['MachineName'] = self.MachineId - # data['Timestamp'] = int(time()) - # data['Platform'] = 1 # raspberrypi platform id is 1 - # data['PacketType'] = PacketTypes.PT_AGENT_CONFIGURATION.value - # self.EnqueuePacket(data) + def Initialize(self): - #debug('CloudServerClient init') + """Initialize server connection and background threads""" try: self.mutex = RLock() self.readQueue = Queue() @@ -378,16 +304,15 @@ def Initialize(self): #start thread only after init of other fields self.sensorsClient = sensors.SensorsClient() self.sensorsClient.SetDataChanged(self.OnDataChanged, self.BuildPT_SYSTEM_INFO) - self.processManager=services.ProcessManager() - self.serviceManager=services.ServiceManager() + self.processManager = services.ProcessManager() + self.serviceManager = services.ServiceManager() self.wifiManager = WifiManager.WifiManager() - self.writerThread = WriterThread('writer',self) + self.writerThread = WriterThread('writer', self) self.writerThread.start() - self.readerThread = ReaderThread('reader',self) + self.readerThread = ReaderThread('reader', self) self.readerThread.start() self.processorThread = ProcessorThread('processor', self) self.processorThread.start() - #TimerThread(self.RequestSchedules, 600, 10) TimerThread(self.CheckConnectionAndPing, self.pingRate) self.sentHistoryData = {} self.historySendFails = 0 @@ -396,7 +321,9 @@ def Initialize(self): self.historyThread.start() except Exception as e: exception('Initialize error: ' + str(e)) + def Destroy(self): + """Destroy client and stop client threads""" info('Shutting down client') self.exiting = True self.sensorsClient.StopMonitoring() @@ -413,90 +340,30 @@ def Destroy(self): ThreadPool.Shutdown() self.Stop() info('Client shut down') - # def Test(self): - # message = {} - # message['PacketType'] = PacketTypes.PT_DEVICE_COMMAND.value - # message['Type'] = '' - # message['Service'] = 'config' - # message['Id']=1021 - # parameters = {} - # parameters['id'] = 16 - # parameters['arguments'] = 'Asia/Tokyo' - # message['Parameters'] = parameters - # self.ExecuteMessage(message) - # message = {} - # message['PacketType'] = PacketTypes.PT_DEVICE_COMMAND.value - # message['Type'] = '' - # message['Service'] = 'config' - # message['Id']=1021 - # parameters = {} - # parameters['id'] = 15 - # parameters['arguments'] = '' - # message['Parameters'] = parameters - # self.ExecuteMessage(message) - # message = {} - # message['PacketType'] = PacketTypes.PT_DEVICE_COMMAND.value - # message['Type'] = '' - # message['Service'] = 'config' - # message['Id']=1021 - # parameters = {} - # parameters['id'] = 0 - # parameters['arguments'] = 'test' - # message['Parameters'] = parameters - # self.ExecuteMessage(message) + def FirstRun(self): - # self.BuildPT_LOCATION() + """Send messages when client is first started""" self.BuildPT_SYSTEM_INFO() - # data = {} - # data['MachineName'] = self.MachineId - # data['Timestamp'] = int(time()) - # data['PacketType'] = PacketTypes.PT_UTILIZATION.value - # self.processManager.RefreshProcessManager() - # data['VisibleMemory'] = 1000000 - # data['AvailableMemory'] = 100000 - # data['AverageProcessorUsage'] = 20 - # data['PeakProcessorUsage'] = 98 - # data['AverageMemoryUsage'] = 30 - # data['PeakMemoryUsage'] = 99 - # data['PercentProcessorTime'] = 80 - # self.EnqueuePacket(data) - # data['PacketType'] = PacketTypes.PT_PROCESS_LIST.value - # self.EnqueuePacket(data) - # data['PacketType'] = PacketTypes.PT_DRIVE_LIST.value - # self.EnqueuePacket(data) - # data['PacketType'] = PacketTypes.PT_PRINTER_INFO.value - # self.EnqueuePacket(data) self.RequestSchedules() - # self.BuildPT_LOCATION() - self.OnUpdateConfig() - def BuildPT_LOCATION(self): - data = {} - data['position'] = {} - data['position']['latitude'] = '30.022112' - data['position']['longitude'] = '45.022112' - data['position']['accuracy'] = '20' - data['position']['method'] = 'Windows location provider' - data['provider'] = 'other' - data['time'] = int(time()) - data['PacketType'] = PacketTypes.PT_LOCATION.value - data['MachineName'] = self.MachineId - self.EnqueuePacket(data) + def BuildPT_UTILIZATION(self): - #debug('BuildPT_UTILIZATION') + """Enqueue a packet containing system utilization data to send to the server""" data = {} data['MachineName'] = self.MachineId data['Timestamp'] = int(time()) data['PacketType'] = PacketTypes.PT_UTILIZATION.value self.processManager.RefreshProcessManager() data['VisibleMemory'] = self.processManager.VisibleMemory - data['AvailableMemory'] = self.processManager.AvailableMemory + data['AvailableMemory'] = self.processManager.AvailableMemory data['AverageProcessorUsage'] = self.processManager.AverageProcessorUsage data['PeakProcessorUsage'] = self.processManager.PeakProcessorUsage data['AverageMemoryUsage'] = self.processManager.AverageMemoryUsage data['PeakMemoryUsage'] = self.processManager.AverageMemoryUsage data['PercentProcessorTime'] = self.processManager.PercentProcessorTime self.EnqueuePacket(data) + def OnDataChanged(self, raspberryValue): + """Enqueue a packet containing system utilization data to send to the server""" data = {} data['MachineName'] = self.MachineId data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value @@ -505,7 +372,9 @@ def OnDataChanged(self, raspberryValue): self.EnqueuePacket(data) del data del raspberryValue + def BuildPT_SYSTEM_INFO(self): + """Enqueue a packet containing system information to send to the server""" try: data = {} data['MachineName'] = self.MachineId @@ -518,7 +387,7 @@ def BuildPT_SYSTEM_INFO(self): raspberryValue['AntiVirus'] = 'None' raspberryValue['Firewall'] = 'iptables' raspberryValue['FirewallEnabled'] = 'true' - raspberryValue['ComputerMake'] = self.hardware.getManufacturer() + raspberryValue['ComputerMake'] = self.hardware.getManufacturer() raspberryValue['ComputerModel'] = self.hardware.getModel() raspberryValue['OsName'] = self.oSInfo.ID raspberryValue['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID @@ -545,13 +414,16 @@ def BuildPT_SYSTEM_INFO(self): logJson('PT_SYSTEM_INFO: ' + dumps(data), 'PT_SYSTEM_INFO') del raspberryValue del data - data=None + data = None except Exception as e: exception('ThreadSystemInfo unexpected error: ' + str(e)) - Debug() + def BuildPT_STARTUP_APPLICATIONS(self): + """Schedule a function to run for retrieving a list of services""" ThreadPool.Submit(self.ThreadServiceManager) + def ThreadServiceManager(self): + """Enqueue a packet containing a list of services to send to the server""" self.serviceManager.Run() sleep(GENERAL_SLEEP_THREAD) data = {} @@ -559,9 +431,13 @@ def ThreadServiceManager(self): data['PacketType'] = PacketTypes.PT_STARTUP_APPLICATIONS.value data['ProcessList'] = self.serviceManager.GetServiceList() self.EnqueuePacket(data) + def BuildPT_PROCESS_LIST(self): + """Schedule a function to run for retrieving a list of processes""" ThreadPool.Submit(self.ThreadProcessManager) + def ThreadProcessManager(self): + """Enqueue a packet containing a list of processes to send to the server""" self.processManager.Run() sleep(GENERAL_SLEEP_THREAD) data = {} @@ -569,10 +445,11 @@ def ThreadProcessManager(self): data['PacketType'] = PacketTypes.PT_PROCESS_LIST.value data['ProcessList'] = self.processManager.GetProcessList() self.EnqueuePacket(data) + def ProcessPT_KILL_PROCESS(self, message): - #debug('ProcessPT_KILL_PROCESS') + """Kill a process specified in message""" pid = message['Pid'] - retVal = self.processManager.KillProcess(int(pid)) + retVal = self.processManager.KillProcess(int(pid)) data = {} data['MachineName'] = self.MachineId data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value @@ -582,8 +459,10 @@ def ProcessPT_KILL_PROCESS(self, message): else: data['Message'] = 'Process not Killed!' self.EnqueuePacket(data) + def CheckSubscription(self): - inviteCode = self.config.get('Agent','InviteCode') + """Check that an invite code is valid""" + inviteCode = self.config.get('Agent', 'InviteCode') cayenneApiClient = CayenneApiClient(self.CayenneApiHost) authId = cayenneApiClient.loginDevice(inviteCode) if authId == None: @@ -593,9 +472,10 @@ def CheckSubscription(self): info('Registration succeeded for invite code {}, auth id = {}'.format(inviteCode, authId)) self.config.set('Agent', 'Initialized', 'true') self.MachineId = authId + @property def Start(self): - #debug('Start') + """Connect to the server""" if self.connected: ret = False error('Start already connected') @@ -603,26 +483,25 @@ def Start(self): info('Connecting to: {}:{}'.format(self.HOST, self.PORT)) count = 0 with self.mutex: - count+=1 + count += 1 while self.connected == False and count < 30: try: - self.sock = None + self.sock = None self.wrappedSocket = None - ##debug('Start wrap_socket') self.sock = socket(AF_INET, SOCK_STREAM) - #self.wrappedSocket = wrap_socket(self.sock, ca_certs="/etc/myDevices/ca.crt", cert_reqs=CERT_REQUIRED) self.wrappedSocket = wrap_socket(self.sock) self.wrappedSocket.connect((self.HOST, self.PORT)) info('myDevices cloud connected') self.connected = True except socket_error as serr: Daemon.OnFailure('cloud', serr.errno) - error ('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(serr)) + error('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(serr)) self.connected = False sleep(30-count) return self.connected + def Stop(self): - #debug('Stop started') + """Disconnect from the server""" Daemon.Reset('cloud') ret = True if self.connected == False: @@ -636,31 +515,33 @@ def Stop(self): info('myDevices cloud disconnected') except socket_error as serr: debug(str(serr)) - error ('myDevices cloud disconnected error:' + str(serr)) + error('myDevices cloud disconnected error:' + str(serr)) ret = False self.connected = False - #debug('Stop finished') return ret + def Restart(self): + """Restart the server connection""" if not self.exiting: debug('Restarting cycle...') sleep(1) self.Stop() self.Start - def SendMessage(self,message): + def SendMessage(self, message): + """Send a message packet to the server""" logJson(message, 'SendMessage') ret = True if self.connected == False: - error('SendMessage fail') - ret = False + error('SendMessage fail') + ret = False else: try: data = bytes(message, 'UTF-8') - max_size=16383 + max_size = 16383 if len(data) > max_size: start = 0 - current=max_size + current = max_size end = len(data) self.wrappedSocket.send(data[start:current]) while current < end: @@ -673,46 +554,49 @@ def SendMessage(self,message): self.onMessageSent(message) message = None except socket_error as serr: - error ('SendMessage:' + str(serr)) + error('SendMessage:' + str(serr)) ret = False Daemon.OnFailure('cloud', serr.errno) sleep(1) except IOError as ioerr: debug('IOError: ' + str(ioerr)) self.Restart() - #Daemon.OnFailure('cloud', ioerr.errno) except socket_error as serr: Daemon.OnFailure('cloud', serr.errno) - except: + except: exception('SendMessage error') return ret + def CheckJson(self, message): + """Check if a JSON message is valid""" try: test = loads(message) except ValueError: return False return True + def ReadMessage(self): + """Read a message from the server""" ret = True if self.connected == False: - ret = False + ret = False else: try: - self.count=4096 - timeout_in_seconds=10 + self.count = 4096 + timeout_in_seconds = 10 ready = select([self.wrappedSocket], [], [], timeout_in_seconds) if ready[0]: message = self.wrappedSocket.recv(self.count).decode() buffering = len(message) == 4096 while buffering and message: - if self.CheckJson(message): - buffering = False - else: - more = self.wrappedSocket.recv(self.count).decode() - if not more: - buffering = False - else: - message += more + if self.CheckJson(message): + buffering = False + else: + more = self.wrappedSocket.recv(self.count).decode() + if not more: + buffering = False + else: + message += more try: if message: messageObject = loads(message) @@ -721,29 +605,31 @@ def ReadMessage(self): else: error('ReadMessage received empty message string') except: - exception('ReadMessage error: ' + str(message)) + exception('ReadMessage error: ' + str(message)) return False Daemon.Reset('cloud') except IOError as ioerr: debug('IOError: ' + str(ioerr)) self.Restart() - #Daemon.OnFailure('cloud', ioerr.errno) except socket_error as serr: Daemon.OnFailure('cloud', serr.errno) except: - exception('ReadMessage error') + exception('ReadMessage error') ret = False sleep(1) Daemon.OnFailure('cloud') return ret + def RunAction(self, action): - #execute action in machine + """Run a specified action""" debug('RunAction') if 'MachineName' in action and self.MachineId != action['MachineName']: debug('Scheduler action is not assigned for this machine: ' + str(action)) return self.ExecuteMessage(action) + def SendNotification(self, notify, subject, body): + """Enqueue a notification message packet to send to the server""" info('SendNotification: ' + str(notify) + ' ' + str(subject) + ' ' + str(body)) try: data = {} @@ -757,19 +643,23 @@ def SendNotification(self, notify, subject, body): debug('') return False return True + def ProcessMessage(self): + """Process a message from the server""" try: messageObject = self.readQueue.get(False) if not messageObject: - return False + return False except Empty: return False with self.mutex: retVal = self.CheckPT_ACK(messageObject) if retVal: return - self.ExecuteMessage(messageObject) + self.ExecuteMessage(messageObject) + def CheckPT_ACK(self, messageObject): + """Check if message is a keep alive packet""" try: packetType = int(messageObject['PacketType']) if packetType == PacketTypes.PT_ACK.value: @@ -779,10 +669,12 @@ def CheckPT_ACK(self, messageObject): debug('') error('CheckPT_ACK failure: ' + str(messageObject)) return False - def ExecuteMessage(self, messageObject): + + def ExecuteMessage(self, messageObject): + """Execute an action described in a message object""" if not messageObject: return - info("ExecuteMessage: " + str(messageObject['PacketType']) ) + info("ExecuteMessage: " + str(messageObject['PacketType'])) packetType = int(messageObject['PacketType']) if packetType == PacketTypes.PT_UTILIZATION.value: self.BuildPT_UTILIZATION() @@ -801,28 +693,20 @@ def ExecuteMessage(self, messageObject): info(PacketTypes.PT_STARTUP_APPLICATIONS) return if packetType == PacketTypes.PT_PROCESS_LIST.value: - self.BuildPT_PROCESS_LIST() + self.BuildPT_PROCESS_LIST() info(PacketTypes.PT_PROCESS_LIST) return if packetType == PacketTypes.PT_KILL_PROCESS.value: - self.ProcessPT_KILL_PROCESS(messageObject) + self.ProcessPT_KILL_PROCESS(messageObject) info(PacketTypes.PT_KILL_PROCESS) - return - if packetType == PacketTypes.PT_INITIALIZED.value: - #self.libMYOPX.SetSubscription(messageObject) - info(PacketTypes.PT_INITIALIZED) - return + return if packetType == PacketTypes.PT_PRODUCT_INFO.value: - self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']); + self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) info(PacketTypes.PT_PRODUCT_INFO) - return - if packetType == PacketTypes.PT_START_RDS_LOCAL_INIT.value: - error('PT_START_RDS_LOCAL_INIT not implemented') - info(PacketTypes.PT_START_RDS_LOCAL_INIT) - return + return if packetType == PacketTypes.PT_RESTART_COMPUTER.value: info(PacketTypes.PT_RESTART_COMPUTER) - data={} + data = {} data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value data['MachineName'] = self.MachineId data['Message'] = 'Computer Restarted!' @@ -832,7 +716,7 @@ def ExecuteMessage(self, messageObject): return if packetType == PacketTypes.PT_SHUTDOWN_COMPUTER.value: info(PacketTypes.PT_SHUTDOWN_COMPUTER) - data={} + data = {} data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value data['MachineName'] = self.MachineId data['Message'] = 'Computer Powered Off!' @@ -840,14 +724,6 @@ def ExecuteMessage(self, messageObject): command = "sudo shutdown -h now" services.ServiceManager.ExecuteCommand(command) return - if packetType == PacketTypes.PT_SUPPORTED_SENSORS.value: - self.sensorsClient.SupportedSensorsUpdate(messageObject) - info(PacketTypes.PT_SUPPORTED_SENSORS) - return - if packetType == PacketTypes.PT_MACHINE_SENSORS.value: - self.sensorsClient.OnDbSensors(messageObject) - info(PacketTypes.PT_MACHINE_SENSORS) - return if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: info('PT_AGENT_CONFIGURATION: ' + str(messageObject.Data)) self.config.setCloudConfig(messageObject.Data) @@ -859,16 +735,16 @@ def ExecuteMessage(self, messageObject): deviceName = None deviceClass = None description = None - #for backward compatibility check the DisplayName and overwrite it over the other variables + #for backward compatibility check the DisplayName and overwrite it over the other variables displayName = None if 'DisplayName' in messageObject: displayName = messageObject['DisplayName'] if 'Parameters' in messageObject: - parameters = messageObject['Parameters'] + parameters = messageObject['Parameters'] if 'DeviceName' in messageObject: - deviceName = messageObject['DeviceName'] + deviceName = messageObject['DeviceName'] else: deviceName = displayName @@ -876,21 +752,21 @@ def ExecuteMessage(self, messageObject): description = messageObject['Description'] else: description = deviceName - + if 'Class' in messageObject: deviceClass = messageObject['Class'] - + retValue = True retValue = self.sensorsClient.AddSensor(deviceName, description, deviceClass, parameters) except Exception as ex: - exception ("PT_ADD_SENSOR Unexpected error"+ str(ex)) + exception("PT_ADD_SENSOR Unexpected error"+ str(ex)) retValue = False data = {} if 'Id' in messageObject: data['Id'] = messageObject['Id'] #0 - None, 1 - Pending, 2-Success, 3 - Not responding, 4 - Failure if retValue: - data['State'] = 2 + data['State'] = 2 else: data['State'] = 4 data['PacketType'] = PacketTypes.PT_UPDATE_SENSOR.value @@ -911,8 +787,8 @@ def ExecuteMessage(self, messageObject): data['Response'] = retValue self.EnqueuePacket(data) except Exception as ex: - exception ("PT_REMOVE_SENSOR Unexpected error"+ str(ex)) - retValue = False + exception("PT_REMOVE_SENSOR Unexpected error"+ str(ex)) + retValue = False return if packetType == PacketTypes.PT_DEVICE_COMMAND.value: info(PacketTypes.PT_DEVICE_COMMAND) @@ -947,7 +823,7 @@ def ExecuteMessage(self, messageObject): if packetType == PacketTypes.PT_UPDATE_SCHEDULES.value: info(PacketTypes.PT_UPDATE_SCHEDULES) retVal = self.schedulerEngine.UpdateSchedules(messageObject) - return + return if packetType == PacketTypes.PT_HISTORY_DATA_RESPONSE.value: info(PacketTypes.PT_HISTORY_DATA_RESPONSE) try: @@ -964,10 +840,9 @@ def ExecuteMessage(self, messageObject): exception('Processing history response packet failed') return info("Skipping not required packet: " + str(packetType)) + def ProcessDeviceCommand(self, messageObject): - # t1 = Thread(target=self.ThreadDeviceCommand) - # t1.start() - # def ThreadDeviceCommand(self): + """Execute a command to run on the device as specified in a message object""" commandType = messageObject['Type'] commandService = messageObject['Service'] parameters = messageObject['Parameters'] @@ -1025,9 +900,9 @@ def ProcessDeviceCommand(self, messageObject): description = sensorName device = None if "Description" in parameters: - description=parameters["Description"] + description = parameters["Description"] if "Args" in parameters: - args=parameters["Args"] + args = parameters["Args"] retValue = self.sensorsClient.EditSensor(sensorName, description, driverClass, args) else: if 'Channel' in parameters: @@ -1038,8 +913,7 @@ def ProcessDeviceCommand(self, messageObject): value = parameters["Value"] if 'SensorType' in parameters: sensorType = parameters["SensorType"] - #(self, commandType, sensorName, sensorType, driverClass, method, channel, value): - retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) + retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) if commandService == 'gpio': method = parameters["Method"] channel = parameters["Channel"] @@ -1055,7 +929,7 @@ def ProcessDeviceCommand(self, messageObject): data["Output"] = output retValue = str(retValue) except: - exception ("Exception on config") + exception("Exception on config") data['Response'] = retValue data['Id'] = id data['PacketType'] = PacketTypes.PT_DEVICE_COMMAND_RESPONSE.value @@ -1064,38 +938,40 @@ def ProcessDeviceCommand(self, messageObject): if sensorId: data['SensorId'] = sensorId self.EnqueuePacket(data) - #if commandService == 'processes': #Kill command is handled with PT_KILL_PROCESS - def EnqueuePacket(self,message): + + def EnqueuePacket(self, message): + """Enqueue a message packet to send to the server""" message['PacketTime'] = GetTime() - #datetime.now().strftime("%Y-%m-%dT%H:%M:%S%z") json_data = dumps(message)+ '\n' message = None - #debug(json_data) self.writeQueue.put(json_data) + def DequeuePacket(self): + """Dequeue a message packet to send to the server""" packet = None try: packet = self.writeQueue.get() except Empty: packet = None return packet + def CheckConnectionAndPing(self): + """Check that the server connection is still alive and send a keep alive packet at intervals""" ticksStart = time() with self.mutex: try: - if(ticksStart - self.lastPing > self.pingTimeout): - #debug('CheckConnectionAndPing EXPIRED - trying to reconnect') + if ticksStart - self.lastPing > self.pingTimeout: self.Stop() self.Start self.lastPing = time() - self.pingRate - 1 warn('Restarting cloud connection -> CheckConnectionAndPing EXPIRED: ' + str(self.lastPing)) - if (ticksStart - self.waitPing >= self.pingRate): - #debug("CheckConnectionAndPing sending ACK packet") + if ticksStart - self.waitPing >= self.pingRate: self.SendAckPacket() except: - debug('') error('CheckConnectionAndPing error') - def SendAckPacket(self): + + def SendAckPacket(self): + """Enqueue a keep alive packet to send to the server""" data = {} debug('Last ping: ' + str(self.lastPing) + ' Wait ping: ' + str(self.waitPing)) data['MachineName'] = self.MachineId @@ -1103,13 +979,17 @@ def SendAckPacket(self): data['PacketType'] = PacketTypes.PT_ACK.value self.EnqueuePacket(data) self.waitPing = time() + def RequestSchedules(self): + """Enqueue a packet to request schedules from the server""" data = {} data['MachineName'] = self.MachineId data['Stored'] = "dynamodb" data['PacketType'] = PacketTypes.PT_REQUEST_SCHEDULES.value self.EnqueuePacket(data) + def SendHistoryData(self): + """Enqueue a packet containing historical data to send to the server""" try: info('SendHistoryData start') history = History() @@ -1136,7 +1016,7 @@ def SendHistoryData(self): info('Sending history data, id = {}'.format(id)) debug('SendHistoryData historyData: ' + str(data)) self.EnqueuePacket(data) - #this will keep acumulating + #this will keep accumulating self.sentHistoryData[id] = data except Exception as ex: exception('SendHistoryData error' + str(ex)) From 9e506ed960288bd7f2a48b5b44c75be348410924 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 13 Jun 2017 18:07:00 -0600 Subject: [PATCH 003/129] Clean up system info code. --- myDevices/os/systeminfo.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/myDevices/os/systeminfo.py b/myDevices/os/systeminfo.py index ac6c793..9823836 100644 --- a/myDevices/os/systeminfo.py +++ b/myDevices/os/systeminfo.py @@ -1,9 +1,10 @@ +""" +This module retrieves information about the system, including CPU, RAM, disk and network data. +""" + import psutil import netifaces -from ctypes import CDLL, c_char_p -from myDevices.utils.logger import exception, info, warn, error, debug, logJson -from os import path, getpid -from json import loads, dumps +from myDevices.utils.logger import exception from myDevices.os.cpu import CpuInfo @@ -23,12 +24,11 @@ def getSystemInformation(self): system_info['Network'] = self.getNetworkInfo() except: exception('Error retrieving system info') - finally: - return system_info + return system_info def getMemoryInfo(self): """Get a dict containing the memory info - + Returned dict example:: { @@ -59,13 +59,13 @@ def getMemoryInfo(self): memory['swap']['total'] = swap.total memory['swap']['free'] = swap.free memory['swap']['used'] = swap.used - except Exception as e: + except: exception('Error getting memory info') return memory def getUptime(self): """Get system uptime as a dict - + Returned dict example:: { @@ -74,21 +74,21 @@ def getUptime(self): } """ info = {} - uptime = 0.0 - idle = 0.0 + uptime = 0.0 + idle = 0.0 try: with open('/proc/uptime', 'r') as f_stat: lines = [line.split(' ') for content in f_stat.readlines() for line in content.split('\n') if line != ''] uptime = float(lines[0][0]) idle = float(lines[0][1]) - except Exception as e: + except: exception('Error getting uptime') info['uptime'] = uptime return info def getDiskInfo(self): """Get system uptime as a dict - + Returned dict example:: { @@ -132,7 +132,7 @@ def getDiskInfo(self): def getNetworkInfo(self): """Get network information as a dict - + Returned dict example:: { From 392f3ec1c5ba5d961ed7e0ad5d6cef956b1acb60 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 10:16:05 -0600 Subject: [PATCH 004/129] Add documentation and clean up code. --- README.rst | 4 +- myDevices/__main__.py | 38 ++-- myDevices/sensors/sensors.py | 215 ++++++++++++++---- .../test/{client_test.py => sensors_test.py} | 4 +- 4 files changed, 190 insertions(+), 71 deletions(-) rename myDevices/test/{client_test.py => sensors_test.py} (97%) diff --git a/README.rst b/README.rst index 2b9c440..a3d2565 100644 --- a/README.rst +++ b/README.rst @@ -127,9 +127,9 @@ To verify that the sensor/actuator works correctly you can test it with the foll * Create a new sensor using ``myDevices.sensors.SensorsClient.AddSensor`` using the appropriate device name and any args required by your device. * Get the sensor values using ``myDevices.sensors.SensorsClient.SensorsInfo`` and make sure the sensor data is correct. * If the new device is an actuator set the actuator value using ``myDevices.sensors.SensorsClient.SensorCommand``. -* Delete the sensor using ``myDevices.sensors.SensorsClient.DeleteSensor``. +* Delete the sensor using ``myDevices.sensors.SensorsClient.RemoveSensor``. -An example demonstrating these functions is available in ``myDevices.test.client_test.py``. +An example demonstrating these functions is available in ``myDevices.test.sensors_test.py``. *Note:* For security reasons the Cayenne agent is designed to be able to run from an account without root privileges. If any of your sensor/actuator code requires root access consider running just that portion of your code via a separate process that can be launched using sudo. For example, the ``myDevices.devices.digital.ds2408`` module uses this method to write data. diff --git a/myDevices/__main__.py b/myDevices/__main__.py index 340eb05..1bb2f5e 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -1,28 +1,30 @@ -"""This module is the main entry """ -from myDevices.utils.config import Config +""" +This module is the main entry point for the Cayenne agent. It processes any command line parameters and launches the client. +""" from os import path, getpid, remove +from sys import __excepthook__, argv, maxsize +from threading import Thread +from myDevices.utils.config import Config from myDevices.cloud.client import CloudServerClient from myDevices.utils.logger import exception, setDebug, info, debug, error, logToFile, setInfo -from sys import excepthook, __excepthook__, argv, maxsize -from threading import Thread from signal import signal, SIGUSR1, SIGINT from resource import getrlimit, setrlimit, RLIMIT_AS from myDevices.os.services import ProcessInfo from myDevices.os.daemon import Daemon -def setMemoryLimit(rsrc, megs = 200): +def setMemoryLimit(rsrc, megs=200): """Set the memory usage limit for the agent process""" size = megs * 1048576 soft, hard = getrlimit(rsrc) setrlimit(rsrc, (size, hard)) #limit to one kilobyte soft, hard = getrlimit(rsrc) - info ('Limit changed to :'+ str( soft)) + try: #Only set memory limit on 32-bit systems if maxsize <= 2**32: setMemoryLimit(RLIMIT_AS) -except Exception as e: - error('Cannot set limit to memory: ' + str(e)) +except Exception as ex: + print('Cannot set limit to memory: ' + str(ex)) client = None pidfile = '/var/run/myDevices/cayenne.pid' @@ -49,7 +51,7 @@ def exceptionHook(exc_type, exc_value, exc_traceback): def threadExceptionHook(): """Make sure any child threads hook exceptions. This should be called before any threads are created.""" - debug('Daemon::threadExceptionHook ') + debug('Daemon::threadExceptionHook') init_original = Thread.__init__ def init(self, *args, **kwargs): init_original(self, *args, **kwargs) @@ -98,37 +100,33 @@ def main(argv): """Main entry point for starting the agent client""" global pidfile configfile = None - scriptfile = None logfile = None - isDebug = False i = 1 setInfo() while i < len(argv): if argv[i] in ["-c", "-C", "--config-file"]: configfile = argv[i+1] - i+=1 + i += 1 elif argv[i] in ["-l", "-L", "--log-file"]: logfile = argv[i+1] - i+=1 + i += 1 elif argv[i] in ["-h", "-H", "--help"]: displayHelp() elif argv[i] in ["-d", "--debug"]: setDebug() elif argv[i] in ["-P", "--pidfile"]: pidfile = argv[i+1] - i+=1 - i+=1 + i += 1 + i += 1 if configfile == None: configfile = '/etc/myDevices/Network.ini' writePidToFile(pidfile) logToFile(logfile) - # SET HOST AND PORT config = Config(configfile) - HOST = config.get('CONFIG','ServerAddress', 'cloud.mydevices.com') - PORT = config.getInt('CONFIG','ServerPort', 8181) + HOST = config.get('CONFIG', 'ServerAddress', 'cloud.mydevices.com') + PORT = config.getInt('CONFIG', 'ServerPort', 8181) CayenneApiHost = config.get('CONFIG', 'CayenneApi', 'https://api.mydevices.com') - # CREATE SOCKET - global client + global client client = CloudServerClient(HOST, PORT, CayenneApiHost) if __name__ == "__main__": diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 2494db0..1e08227 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -1,18 +1,19 @@ +""" +This module provides a class for interfacing with sensors and actuators. It can add, edit and remove +sensors and actuators as well as monitor their states and execute commands. +""" from myDevices.utils.logger import exception, info, warn, error, debug, logJson from time import sleep, time -from json import loads,dumps +from json import loads, dumps from threading import Thread, RLock from myDevices.os import services -from copy import deepcopy from datetime import datetime, timedelta from os import path, getpid from urllib import parse -import http.client as httplib from myDevices.os.daemon import Daemon from myDevices.cloud.dbmanager import DbManager from myDevices.os.threadpool import ThreadPool from hashlib import sha1 -from http.client import CannotSendRequest import urllib.request as req from myDevices.utils.version import MAPPING from myDevices.devices.bus import checkAllBus, BUSLIST @@ -26,7 +27,10 @@ SENSOR_INFO_SLEEP = 0.05 class SensorsClient(): + """Class for interfacing with sensors and actuators""" + def __init__(self): + """Initialize the bus and sensor info and start monitoring sensor states""" self.sensorMutex = RLock() self.systemMutex = RLock() self.continueMonitoring = False @@ -49,18 +53,28 @@ def __init__(self): for row in results: self.disabledSensors[row[0]] = 1 self.StartMonitoring() - def SetDataChanged(self, onDataChanged = None, onSystemInfo = None): - #one parameter only that contains data in a dictionary for changed items only + + def SetDataChanged(self, onDataChanged=None, onSystemInfo=None): + """Set callbacks to call when data has changed + + Args: + onDataChanged: Function to call when sensor data changes + onSystemInfo: Function to call when system info changes + """ self.onDataChanged = onDataChanged self.onSystemInfo = onSystemInfo + def StartMonitoring(self): + """Start thread monitoring sensor data""" self.continueMonitoring = True - #thread = Thread(target = self.Monitor) - #thread.start() ThreadPool.Submit(self.Monitor) + def StopMonitoring(self): + """Stop thread monitoring sensor data""" self.continueMonitoring = False + def Monitor(self): + """Monitor bus/sensor states and system info and report changed data via callbacks""" nextTime = datetime.now() nextTimeSystemInfo = datetime.now() debug('Monitoring sensors and os resources started') @@ -77,37 +91,40 @@ def Monitor(self): nextTimeSystemInfo = datetime.now() + timedelta(seconds=5) self.MonitorSensors() self.MonitorBus() - if self.onDataChanged != None: + if self.onDataChanged and self.raspberryValue: self.onDataChanged(self.raspberryValue) - bResult=self.RemoveRefresh(refreshTime) - if bResult == True and self.onSystemInfo != None: + bResult = self.RemoveRefresh(refreshTime) + if bResult and self.onSystemInfo: self.onSystemInfo() self.sensorsRefreshCount += 1 nextTime = datetime.now() + timedelta(seconds=REFRESH_FREQUENCY) sleep(REFRESH_FREQUENCY) except: - exception("Monitoring sensors and os resources failed: " + str() ) + exception("Monitoring sensors and os resources failed: " + str()) debug('Monitoring sensors and os resources Finished') + def MonitorSensors(self): - if self.continueMonitoring == False: + """Check sensor states for changes""" + if not self.continueMonitoring: return debug(str(time()) + ' Get sensors info ' + str(self.sensorsRefreshCount)) self.SensorsInfo() debug(str(time()) + ' Got sensors info ' + str(self.sensorsRefreshCount)) with self.sensorMutex: if self.SHA_Calc(self.currentSensorsInfo) != self.SHA_Calc(self.previousSensorsInfo): - #do merge mergedSensors = self.ChangedSensorsList() if self.previousSensorsInfo: del self.previousSensorsInfo self.previousSensorsInfo = None - if mergedSensors == None: + if mergedSensors is None: self.previousSensorsInfo = self.currentSensorsInfo return self.raspberryValue['SensorsInfo'] = mergedSensors self.previousSensorsInfo = self.currentSensorsInfo debug(str(time()) + ' Merge sensors info ' + str(self.sensorsRefreshCount)) + def ChangedSensorsList(self): + """Return list of changed sensors""" if self.continueMonitoring == False: return None if self.previousSensorsInfo == None: @@ -125,7 +142,9 @@ def ChangedSensorsList(self): else: changedSensors.append(item) return changedSensors + def MonitorBus(self): + """Check bus states for changes""" if self.continueMonitoring == False: return debug(str(time()) + ' Get bus info ' + str(self.sensorsRefreshCount)) @@ -133,17 +152,19 @@ def MonitorBus(self): debug(str(time()) + ' Got bus info ' + str(self.sensorsRefreshCount)) if self.SHA_Calc(self.currentBusInfo) != self.SHA_Calc(self.previousBusInfo): self.raspberryValue['BusInfo'] = self.currentBusInfo - if self.previousBusInfo: + if self.previousBusInfo: del self.previousBusInfo self.previousBusInfo = None self.previousBusInfo = self.currentBusInfo + def MonitorSystemInformation(self): + """Check system info for changes""" if self.continueMonitoring == False: return debug(str(time()) + ' Get system info ' + str(self.sensorsRefreshCount)) self.SystemInformation() debug(str(time()) + ' Got system info ' + str(self.sensorsRefreshCount)) - firstSHA = self.SHA_Calc(self.currentSystemInfo) + firstSHA = self.SHA_Calc(self.currentSystemInfo) secondSHA = self.SHA_Calc(self.previousSystemInfo) if firstSHA != secondSHA: if self.previousSystemInfo: @@ -151,9 +172,11 @@ def MonitorSystemInformation(self): self.previousSystemInfo = None self.previousSystemInfo = self.currentSystemInfo self.raspberryValue['SystemInfo'] = self.currentSystemInfo + def SystemInformation(self): + """Return dict containing current system info, including CPU, RAM, storage and network info""" with self.systemMutex: - self.retrievingSystemInfo = True + self.retrievingSystemInfo = True try: systemInfo = SystemInfo() newSystemInfo = systemInfo.getSystemInformation() @@ -162,9 +185,11 @@ def SystemInformation(self): except Exception as ex: exception('SystemInformation failed: '+str(ex)) with self.systemMutex: - self.retrievingSystemInfo = False + self.retrievingSystemInfo = False return self.currentSystemInfo + def SHA_Calc(self, object): + """Return SHA value for an object""" if object == None: return '' try: @@ -173,12 +198,22 @@ def SHA_Calc(self, object): exception('SHA_Calc failed for:' + str(object)) return '' return self.SHA_Calc_str(strVal) + def SHA_Calc_str(self, stringVal): + """Return SHA value for a string""" m = sha1() m.update(stringVal.encode('utf8')) sDigest = str(m.hexdigest()) return sDigest + def AppendToDeviceList(self, device_list, source, device_type): + """Append a sensor/actuator device to device list + + Args: + device_list: Device list to append device to + source: Device to append to list + device_type: Type of device + """ device = source.copy() del device['origin'] device['name'] = parse.unquote(device['name']) @@ -192,7 +227,9 @@ def AppendToDeviceList(self, device_list, source, device_type): else: device['enabled'] = 1 device_list.append(device) + def GetDevices(self): + """Return a list of current sensor/actuator devices""" device_list = manager.getDeviceList() devices = [] for dev in device_list: @@ -203,9 +240,19 @@ def GetDevices(self): for device_type in dev['type']: self.AppendToDeviceList(devices, dev, device_type) except: - exception ("Failed to get device: {}".format(dev)) + exception("Failed to get device: {}".format(dev)) return devices + def CallDeviceFunction(self, func, *args): + """Call a function for a sensor/actuator device and format the result value type + + Args: + func: Function to call + args: Parameters to pass to the function + + Returns: + True for success, False otherwise. + """ result = func(*args) if result != None: if hasattr(func, "contentType"): @@ -217,7 +264,9 @@ def CallDeviceFunction(self, func, *args): else: response = result return response + def BusInfo(self): + """Return a dict with current bus info""" json = {} for (bus, value) in BUSLIST.items(): json[bus] = int(value["enabled"]) @@ -230,7 +279,9 @@ def BusInfo(self): json['GpioMap'] = MAPPING self.currentBusInfo = json return self.currentBusInfo + def SensorsInfo(self): + """Return a dict with current sensor states for all enabled sensors""" with self.sensorMutex: devices = self.GetDevices() debug(str(time()) + ' Got devices info ' + str(self.sensorsRefreshCount)) @@ -244,7 +295,7 @@ def SensorsInfo(self): if value['type'] == 'Temperature': value['Celsius'] = self.CallDeviceFunction(sensor.getCelsius) value['Fahrenheit'] = self.CallDeviceFunction(sensor.getFahrenheit) - value['Kelvin'] =self.CallDeviceFunction(sensor.getKelvin) + value['Kelvin'] = self.CallDeviceFunction(sensor.getKelvin) if value['type'] == 'Pressure': value['Pascal'] = self.CallDeviceFunction(sensor.getPascal) if value['type'] == 'Luminosity': @@ -259,7 +310,7 @@ def SensorsInfo(self): value['allInteger'] = self.CallDeviceFunction(sensor.analogReadAll) value['allVolt'] = self.CallDeviceFunction(sensor.analogReadAllVolt) value['allFloat'] = self.CallDeviceFunction(sensor.analogReadAllFloat) - if value['type'] in ('DAC'): + if value['type'] in 'DAC': value['vref'] = self.CallDeviceFunction(sensor.analogReference) if value['type'] == 'PWM': value['channelCount'] = self.CallDeviceFunction(sensor.pwmCount) @@ -267,8 +318,8 @@ def SensorsInfo(self): value['resolution'] = self.CallDeviceFunction(sensor.pwmResolution) value['all'] = self.CallDeviceFunction(sensor.pwmWildcard) if value['type'] == 'Humidity': - value['float']=self.CallDeviceFunction(sensor.getHumidity) - value['percent']=self.CallDeviceFunction(sensor.getHumidityPercent) + value['float'] = self.CallDeviceFunction(sensor.getHumidity) + value['percent'] = self.CallDeviceFunction(sensor.getHumidityPercent) if value['type'] == 'PiFaceDigital': value['all'] = self.CallDeviceFunction(sensor.readAll) if value['type'] in ('DigitalSensor', 'DigitalActuator'): @@ -285,7 +336,7 @@ def SensorsInfo(self): if value['type'] == 'AnalogActuator': value['float'] = self.CallDeviceFunction(sensor.readFloat) except: - exception ("Sensor values failed: "+ value['type'] + " " + value['name']) + exception("Sensor values failed: "+ value['type'] + " " + value['name']) try: if 'hash' in value: value['sensor'] = value['hash'] @@ -302,7 +353,19 @@ def SensorsInfo(self): debug(('New sensors info retrieved: {}').format(self.sensorsRefreshCount)) logJson('Sensors Info updated: ' + str(self.currentSensorsInfo)) return self.currentSensorsInfo + def AddSensor(self, name, description, device, args): + """Add a new sensor/actuator + + Args: + name: Name of sensor to add + description: Sensor description + device: Sensor device class + args: Sensor specific args + + Returns: + True for success, False otherwise. + """ info('AddSensor: {}, {}, {}, {}'.format(name, description, device, args)) bVal = False try: @@ -324,11 +387,23 @@ def AddSensor(self, name, description, device, args): except: bVal = False return bVal + def EditSensor(self, name, description, device, args): + """Edit an existing sensor/actuator + + Args: + name: Name of sensor to edit + description: New sensor description + device: New sensor device class + args: New sensor specific args + + Returns: + True for success, False otherwise. + """ info('EditSensor: {}, {}, {}, {}'.format(name, description, device, args)) bVal = False try: - sensorEdit= {} + sensorEdit = {} name = req.pathname2url(name) sensorEdit['name'] = name sensorEdit['device'] = device @@ -349,7 +424,7 @@ def EditSensor(self, name, description, device, args): sensor['description'] = description raspberryValue['SensorsInfo'] = [] raspberryValue['SensorsInfo'].append(sensor) - if self.onDataChanged != None: + if self.onDataChanged: self.onDataChanged(raspberryValue) except: pass @@ -357,10 +432,19 @@ def EditSensor(self, name, description, device, args): bVal = True self.AddRefresh() except: - exception ("Edit sensor failed") + exception("Edit sensor failed") bVal = False return bVal - def DeleteSensor(self, name): + + def RemoveSensor(self, name): + """Remove an existing sensor/actuator + + Args: + name: Name of sensor to remove + + Returns: + True for success, False otherwise. + """ bVal = False try: sensorRemove = req.pathname2url(name) @@ -371,14 +455,20 @@ def DeleteSensor(self, name): bVal = True self.AddRefresh() except: - exception ("Remove sensor failed") + exception("Remove sensor failed") bVal = False return bVal - def RemoveSensor(self, name): - debug('') - return self.DeleteSensor(name) + def EnableSensor(self, sensor, enable): - #sensor is the hash composed from name and device class/type + """Enable a sensor/actuator + + Args: + sensor: Hash composed from name and device class/type + enable: 1 to enable, 0 to disable + + Returns: + True for success, False otherwise. + """ info('Enable sensor: ' + str(sensor) + ' ' + str(enable)) try: if sensor is None: @@ -402,17 +492,39 @@ def EnableSensor(self, sensor, enable): return False self.AddRefresh() return True + def AddRefresh(self): + """Add the time to list of system info changed times""" self.systemInfoRefreshList.append(int(time())) - def RemoveRefresh(self, newRefresh): + + def RemoveRefresh(self, cutoff): + """Remove times from refresh list and check if system info was changed + + Args: + cutoff: Cutoff time to use when checking for system info changes + + Returns: + True if system info has changed before cutoff, False otherwise. + """ bReturn = False for i in self.systemInfoRefreshList: - if i < newRefresh: + if i < cutoff: self.systemInfoRefreshList.remove(i) bReturn = True return bReturn + def GpioCommand(self, commandType, method, channel, value): - debug('') + """Execute onboard GPIO command + + Args: + commandType: Type of command to execute + method: 'POST' for setting/writing values, 'GET' for retrieving values + channel: GPIO pin + value: Value to use for reading/writing data + + Returns: + String containing command specific return value on success, or 'failure' on failure + """ info('GpioCommand ' + commandType + ' method ' + method + ' Channel: ' + str(channel) + ' Value: ' + str(value)) if commandType == 'function': if method == 'POST': @@ -434,18 +546,32 @@ def GpioCommand(self, commandType, method, channel, value): debug('portWrite:' + str(value)) return str(self.gpio.portWrite(value)) if method == 'GET': - debug('portRead:' ) + debug('portRead') return str(self.gpio.portRead()) debug.log('GpioCommand not set') return 'failure' + def SensorCommand(self, commandType, sensorName, sensorType, driverClass, method, channel, value): + """Execute sensor/actuator command + + Args: + commandType: Type of command to execute + sensorName: Name of the sensor + sensorType: Type of the sensor + driverClass: Class of device + method: Not currently used + channel: Pin/channel on device + value: Value to use for sending data + + Returns: + Command specific return value on success, False on failure + """ retVal = False info('SensorCommand: {} SensorName {} SensorType {} DriverClass {} Method {} Channel {} Value {}'.format(commandType, sensorName, sensorType, driverClass, method, channel, value) ) try: self.AddRefresh() - debug('') - actuators=('GPIOPort', 'ServoMotor', 'AnalogActuator', 'LoadSensor', 'PiFaceDigital', 'DistanceSensor', 'Thermistor', 'Photoresistor', 'LightDimmer', 'LightSwitch', 'DigitalSensor', 'DigitalActuator', 'MotorSwitch', 'RelaySwitch', 'ValveSwitch', 'MotionSensor') - gpioExtensions=('GPIOPort', 'PiFaceDigital') + actuators = ('GPIOPort', 'ServoMotor', 'AnalogActuator', 'LoadSensor', 'PiFaceDigital', 'DistanceSensor', 'Thermistor', 'Photoresistor', 'LightDimmer', 'LightSwitch', 'DigitalSensor', 'DigitalActuator', 'MotorSwitch', 'RelaySwitch', 'ValveSwitch', 'MotionSensor') + gpioExtensions = ('GPIOPort', 'PiFaceDigital') if driverClass is None: hashKey = self.SHA_Calc_str(sensorName+sensorType) else: @@ -491,10 +617,5 @@ def SensorCommand(self, commandType, sensorName, sensorType, driverClass, method return retVal except Exception as ex: exception('SensorCommand failed with: ' +str(ex)) - pass - finally: - #looks like this breaks actuators refresh by updating and not sending data changed - #then refresh never comes for the specific sensor - #ThreadPool.SubmitParam(self.Monitor, hashKey) - return retVal + return retVal diff --git a/myDevices/test/client_test.py b/myDevices/test/sensors_test.py similarity index 97% rename from myDevices/test/client_test.py rename to myDevices/test/sensors_test.py index 872d3fe..8362888 100644 --- a/myDevices/test/client_test.py +++ b/myDevices/test/sensors_test.py @@ -63,7 +63,7 @@ def testSensors(self): for key in compareKeys: self.assertEqual(editedSensor[key], retrievedSensor[key]) #Test removing a sensor - retValue = SensorsClientTest.client.DeleteSensor(testSensor['name']) + retValue = SensorsClientTest.client.RemoveSensor(testSensor['name']) self.assertTrue(retValue) deviceNames = [device['name'] for device in SensorsClientTest.client.GetDevices()] self.assertNotIn(testSensor['name'], deviceNames) @@ -82,7 +82,7 @@ def testSensorInfo(self): retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensors['distance']['name']) self.assertEqual(retrievedSensorInfo['float'], 0.0) for sensor in sensors.values(): - self.assertTrue(SensorsClientTest.client.DeleteSensor(sensor['name'])) + self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) def testSystemInfo(self): system_info = SensorsClientTest.client.SystemInformation() From 5fd04a337dd6fec1afc9bf53b437cce01e4fef9e Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 10:58:27 -0600 Subject: [PATCH 005/129] Clean up thread pool code. --- myDevices/os/threadpool.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/myDevices/os/threadpool.py b/myDevices/os/threadpool.py index 96879e9..b0f59c0 100644 --- a/myDevices/os/threadpool.py +++ b/myDevices/os/threadpool.py @@ -1,12 +1,20 @@ -from concurrent.futures import ThreadPoolExecutor +""" +This module provides a singleton thread pool class +""" +from concurrent.futures import ThreadPoolExecutor from myDevices.utils.singleton import Singleton -import inspect executor = ThreadPoolExecutor(max_workers=4) class ThreadPool(Singleton): - def Submit(something): - future = executor.submit(something) - def SubmitParam(*arg): - executor.submit(*arg) - def Shutdown(): - executor.shutdown() \ No newline at end of file + """Singleton thread pool class""" + + @staticmethod + def Submit(func): + """Submit a function for the thread pool to run""" + executor.submit(func) + + @staticmethod + def Shutdown(): + """Shutdown the thread pool""" + executor.shutdown() + \ No newline at end of file From b725068491b7e8d344ab6de26214cebd1ebd69cc Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 11:40:43 -0600 Subject: [PATCH 006/129] Move threadpool to utils. --- myDevices/cloud/client.py | 2 +- myDevices/cloud/download_speed.py | 2 +- myDevices/os/raspiconfig.py | 2 +- myDevices/sensors/sensors.py | 2 +- myDevices/utils/logger.py | 2 +- myDevices/{os => utils}/threadpool.py | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename myDevices/{os => utils}/threadpool.py (100%) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 10a7b58..ba87e19 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -24,7 +24,7 @@ from myDevices.cloud.updater import Updater from myDevices.os.raspiconfig import RaspiConfig from myDevices.os.daemon import Daemon -from myDevices.os.threadpool import ThreadPool +from myDevices.utils.threadpool import ThreadPool from myDevices.utils.history import History from select import select from hashlib import sha256 diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index 3090308..8fe1c34 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -9,7 +9,7 @@ from random import randint from socket import error as socket_error from myDevices.os.daemon import Daemon -from myDevices.os.threadpool import ThreadPool +from myDevices.utils.threadpool import ThreadPool defaultUrl = "https://updates.mydevices.com/test/10MB.zip" download_path = "/etc/myDevices/test" diff --git a/myDevices/os/raspiconfig.py b/myDevices/os/raspiconfig.py index cdddf05..db0eb54 100644 --- a/myDevices/os/raspiconfig.py +++ b/myDevices/os/raspiconfig.py @@ -1,7 +1,7 @@ from myDevices.utils.logger import exception, info, warn, error, debug from myDevices.os.services import ServiceManager from time import sleep -from myDevices.os.threadpool import ThreadPool +from myDevices.utils.threadpool import ThreadPool CUSTOM_CONFIG_SCRIPT = "/etc/myDevices/scripts/config.sh" diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 1e08227..1587def 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -12,7 +12,7 @@ from urllib import parse from myDevices.os.daemon import Daemon from myDevices.cloud.dbmanager import DbManager -from myDevices.os.threadpool import ThreadPool +from myDevices.utils.threadpool import ThreadPool from hashlib import sha1 import urllib.request as req from myDevices.utils.version import MAPPING diff --git a/myDevices/utils/logger.py b/myDevices/utils/logger.py index e7bd49c..30a2bd8 100644 --- a/myDevices/utils/logger.py +++ b/myDevices/utils/logger.py @@ -7,7 +7,7 @@ from datetime import datetime from hashlib import sha256 import time -from myDevices.os.threadpool import ThreadPool +from myDevices.utils.threadpool import ThreadPool from glob import iglob MAX_JSON_ITEMS_PER_CATEGORY = 100 diff --git a/myDevices/os/threadpool.py b/myDevices/utils/threadpool.py similarity index 100% rename from myDevices/os/threadpool.py rename to myDevices/utils/threadpool.py From eebafcf8a7a34108fbae12645e469979496403ab Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 11:55:12 -0600 Subject: [PATCH 007/129] Add documentation and clean up daemon code. --- myDevices/os/daemon.py | 91 ++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/myDevices/os/daemon.py b/myDevices/os/daemon.py index 603bbe4..5e4ca22 100644 --- a/myDevices/os/daemon.py +++ b/myDevices/os/daemon.py @@ -1,48 +1,63 @@ -#!/usr/bin/env python +""" +This module provides a class for restarting the agent if errors occur and exiting on critical failures. +""" from sys import exit from datetime import datetime -from os.path import getmtime from myDevices.utils.logger import exception, info, warn, error, debug from myDevices.os.services import ServiceManager #defining reset timeout in seconds -RESET_TIMEOUT=30 -FAILURE_COUNT=1000 -PYTHON_BIN='/usr/bin/python3' -failureCount={} -startFailure={} -errorList= (-3, -2, 12, 9, 24) +RESET_TIMEOUT = 30 +FAILURE_COUNT = 1000 +failureCount = {} +startFailure = {} +errorList = (-3, -2, 12, 9, 24) + + class Daemon: - def OnFailure(component, error=0): - #-3=Temporary failure in name resolution - info('Daemon failure handling ' + str(error)) - if error in errorList: - Daemon.Restart() - if component not in failureCount: - Daemon.Reset(component) - failureCount[component]+=1 - now = datetime.now() - if startFailure[component]==0: - startFailure[component]=now - elapsedTime=now-startFailure[component] - if (elapsedTime.total_seconds() >= RESET_TIMEOUT) or ( failureCount[component] > FAILURE_COUNT): - warn('Daemon::OnFailure myDevices is going to restart after ' +str(component) + ' failed: ' + str(elapsedTime.total_seconds()) + ' seconds and ' + str(failureCount) + ' times') - Daemon.Restart() - def Reset(component): - startFailure[component]=0 - failureCount[component]=0 - def Restart(): - try: - info('Daemon Restarting myDevices' ) - (output, returncode) = ServiceManager.ExecuteCommand('sudo service myDevices restart') - debug(str(output) + ' ' + str(returncode)) - del output - except: - exception ("Daemon::Restart Unexpected error") - Daemon.Exit() - def Exit(): - info('Critical failure. Closing myDevices process...') - exit('Daemon::Exit Closing agent. Critical failure.') + """class for restarting the agent if errors occur and exiting on critical failures.""" + + @staticmethod + def OnFailure(component, error=0): + """Handle error in component and restart the agent if necessary""" + #-3=Temporary failure in name resolution + info('Daemon failure handling ' + str(error)) + if error in errorList: + Daemon.Restart() + if component not in failureCount: + Daemon.Reset(component) + failureCount[component] += 1 + now = datetime.now() + if startFailure[component] == 0: + startFailure[component] = now + elapsedTime = now - startFailure[component] + if (elapsedTime.total_seconds() >= RESET_TIMEOUT) or (failureCount[component] > FAILURE_COUNT): + warn('Daemon::OnFailure myDevices is going to restart after ' + str(component) + ' failed: ' + str(elapsedTime.total_seconds()) + ' seconds and ' + str(failureCount) + ' times') + Daemon.Restart() + + @staticmethod + def Reset(component): + """Reset failure count for component""" + startFailure[component] = 0 + failureCount[component] = 0 + + @staticmethod + def Restart(): + """Restart the agent daemon""" + try: + info('Daemon restarting myDevices' ) + (output, returncode) = ServiceManager.ExecuteCommand('sudo service myDevices restart') + debug(str(output) + ' ' + str(returncode)) + del output + except: + exception("Daemon::Restart enexpected error") + Daemon.Exit() + + @staticmethod + def Exit(): + """Stop the agent daemon""" + info('Critical failure. Closing myDevices process...') + exit('Daemon::Exit closing agent. Critical failure.') From 51a953609882cd6fee53fdce455faea177b5df4a Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 11:57:57 -0600 Subject: [PATCH 008/129] Move daemon to utils. --- myDevices/__main__.py | 2 +- myDevices/cloud/client.py | 2 +- myDevices/cloud/download_speed.py | 2 +- myDevices/os/services.py | 2 +- myDevices/sensors/sensors.py | 2 +- myDevices/{os => utils}/daemon.py | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename myDevices/{os => utils}/daemon.py (100%) diff --git a/myDevices/__main__.py b/myDevices/__main__.py index 1bb2f5e..0f8c8b5 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -10,7 +10,7 @@ from signal import signal, SIGUSR1, SIGINT from resource import getrlimit, setrlimit, RLIMIT_AS from myDevices.os.services import ProcessInfo -from myDevices.os.daemon import Daemon +from myDevices.utils.daemon import Daemon def setMemoryLimit(rsrc, megs=200): """Set the memory usage limit for the agent process""" diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ba87e19..456f3c7 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -23,7 +23,7 @@ from myDevices.cloud.download_speed import DownloadSpeed from myDevices.cloud.updater import Updater from myDevices.os.raspiconfig import RaspiConfig -from myDevices.os.daemon import Daemon +from myDevices.utils.daemon import Daemon from myDevices.utils.threadpool import ThreadPool from myDevices.utils.history import History from select import select diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index 8fe1c34..3b14cde 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -8,7 +8,7 @@ from time import sleep from random import randint from socket import error as socket_error -from myDevices.os.daemon import Daemon +from myDevices.utils.daemon import Daemon from myDevices.utils.threadpool import ThreadPool defaultUrl = "https://updates.mydevices.com/test/10MB.zip" diff --git a/myDevices/os/services.py b/myDevices/os/services.py index 1f185d9..e68bfe4 100644 --- a/myDevices/os/services.py +++ b/myDevices/os/services.py @@ -258,7 +258,7 @@ def ExecuteCommand(command, increaseMemoryLimit=False): processOutput = None except OSError as oserror: warn('ServiceManager::ExecuteCommand handled: ' + command + ' Exception:' + str(traceback.format_exc())) - from myDevices.os.daemon import Daemon + from myDevices.utils.daemon import Daemon Daemon.OnFailure('services', oserror.errno) except: exception('ServiceManager::ExecuteCommand failed: ' + command) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 1587def..600ec5f 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from os import path, getpid from urllib import parse -from myDevices.os.daemon import Daemon +from myDevices.utils.daemon import Daemon from myDevices.cloud.dbmanager import DbManager from myDevices.utils.threadpool import ThreadPool from hashlib import sha1 diff --git a/myDevices/os/daemon.py b/myDevices/utils/daemon.py similarity index 100% rename from myDevices/os/daemon.py rename to myDevices/utils/daemon.py From 2f7c346d329c3e5567a6a9d0e138e3646df9a672 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 12:22:07 -0600 Subject: [PATCH 009/129] Move ipgetter to os. --- myDevices/__init__.py | 6 ------ myDevices/cloud/client.py | 5 ++--- .../{ipgetter/__init__.py => os/ipgetter.py} | 15 ++++++--------- setup.py | 2 +- 4 files changed, 9 insertions(+), 19 deletions(-) rename myDevices/{ipgetter/__init__.py => os/ipgetter.py} (94%) diff --git a/myDevices/__init__.py b/myDevices/__init__.py index 122f144..8b13789 100644 --- a/myDevices/__init__.py +++ b/myDevices/__init__.py @@ -1,7 +1 @@ -from time import sleep - -try: - import ipgetter -except: - pass diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 456f3c7..2c8e345 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -13,9 +13,8 @@ from queue import Queue, Empty from enum import Enum, unique from myDevices.utils.config import Config -import myDevices.ipgetter from myDevices.utils.logger import exception, info, warn, error, debug, logJson -from myDevices.os import services +from myDevices.os import services, ipgetter from myDevices.sensors import sensors from myDevices.os.hardware import Hardware from myDevices.wifi import WifiManager @@ -291,7 +290,7 @@ def Initialize(self): self.pingTimeout = 35 self.waitPing = 0 self.lastPing = time()-self.pingRate - 1 - self.PublicIP = myDevices.ipgetter.myip() + self.PublicIP = ipgetter.myip() self.hardware = Hardware() self.oSInfo = OSInfo() self.downloadSpeed = DownloadSpeed(self.config) diff --git a/myDevices/ipgetter/__init__.py b/myDevices/os/ipgetter.py similarity index 94% rename from myDevices/ipgetter/__init__.py rename to myDevices/os/ipgetter.py index 5f8f95c..519389e 100644 --- a/myDevices/ipgetter/__init__.py +++ b/myDevices/os/ipgetter.py @@ -19,8 +19,8 @@ >>> ipgetter.IPgetter().test() Number of servers: 47 - IP's : - 8.8.8.8 = 47 ocurrencies + IPs: + 8.8.8.8 = 47 occurrences Copyright 2014 phoemur@gmail.com @@ -49,7 +49,6 @@ def myip(): class IPgetter(object): - ''' This class is designed to fetch your external IP address from the internet. It is used mostly when behind a NAT. @@ -108,7 +107,6 @@ def get_externalip(self): ''' This function gets your IP from a random server ''' - myip = '' for i in range(7): myip = self.fetch(choice(self.server_list)) @@ -131,7 +129,7 @@ def fetch(self, server): url = opener.open(server, timeout=2) content = url.read() - # Didn't want to import chardet. Prefered to stick to stdlib + # Didn't want to import chardet. Preferred to stick to stdlib if PY3K: try: content = content.decode('UTF-8') @@ -155,7 +153,6 @@ def test(self): on the list when retrieving your IP. All results should be the same. ''' - resultdict = {} for server in self.server_list: resultdict.update(**{server: self.fetch(server)}) @@ -163,9 +160,9 @@ def test(self): ips = sorted(resultdict.values()) ips_set = set(ips) print('\nNumber of servers: {}'.format(len(self.server_list))) - print("IP's :") - for ip, ocorrencia in zip(ips_set, map(lambda x: ips.count(x), ips_set)): - print('{0} = {1} ocurrenc{2}'.format(ip if len(ip) > 0 else 'broken server', ocorrencia, 'y' if ocorrencia == 1 else 'ies')) + print("IPs:") + for ip, occurrence in zip(ips_set, map(lambda x: ips.count(x), ips_set)): + print(' {0} = {1} occurrences'.format(ip if len(ip) > 0 else 'broken server', occurrence)) print('\n') print(resultdict) diff --git a/setup.py b/setup.py index 644f4e7..d471551 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ keywords = 'myDevices Cayenne IoT', url = 'https://www.mydevices.com/', classifiers = classifiers, - packages = ["myDevices", "myDevices.ipgetter", "myDevices.cloud", "myDevices.utils", "myDevices.os", "myDevices.sensors" , "myDevices.wifi", "myDevices.schedule", "myDevices.requests_futures", "myDevices.devices", "myDevices.devices.analog", "myDevices.devices.digital", "myDevices.devices.sensor", "myDevices.devices.shield", "myDevices.decorators"], + packages = ["myDevices", "myDevices.cloud", "myDevices.utils", "myDevices.os", "myDevices.sensors" , "myDevices.wifi", "myDevices.schedule", "myDevices.requests_futures", "myDevices.devices", "myDevices.devices.analog", "myDevices.devices.digital", "myDevices.devices.sensor", "myDevices.devices.shield", "myDevices.decorators"], install_requires = ['enum34', 'iwlib', 'jsonpickle', 'netifaces', 'psutil >= 0.7.0', 'requests'], data_files = [('/etc/myDevices/scripts', ['scripts/config.sh'])] ) From 83d8bd87194ae2a69bb4e30c35e7ab90d31e193a Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 17:35:03 -0600 Subject: [PATCH 010/129] Move board version and pin mapping code to os module. --- README.rst | 2 +- myDevices/devices/bus.py | 2 +- myDevices/devices/i2c.py | 2 +- myDevices/devices/spi.py | 4 +-- myDevices/os/hardware.py | 57 ++++++++++++++++++++++++--------- myDevices/os/version.py | 16 +++++++++ myDevices/sensors/sensors.py | 2 +- myDevices/test/hardware_test.py | 38 ++++++++++++++++++++++ myDevices/test/sensors_test.py | 1 - myDevices/utils/version.py | 47 --------------------------- 10 files changed, 102 insertions(+), 69 deletions(-) create mode 100644 myDevices/os/version.py create mode 100644 myDevices/test/hardware_test.py delete mode 100644 myDevices/utils/version.py diff --git a/README.rst b/README.rst index a3d2565..db70471 100644 --- a/README.rst +++ b/README.rst @@ -170,7 +170,7 @@ Hardware Info Hardware info, including make, model, etc. is retrieved via ``myDevices.os.hardware.py``. This should be modified or overridden to provide the appropriate hardware info for your board. Pin Mapping - The mapping of the on-board pins is provided in ``myDevices.utils.version.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab. + The mapping of the on-board pins is provided in ``myDevices.os.hardware.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab. Settings -------- diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index a25e045..383d746 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -17,7 +17,7 @@ import subprocess from myDevices.utils.logger import debug, info -from myDevices.utils.version import OS_VERSION, OS_RASPBIAN_JESSIE, OS_RASPBIAN_WHEEZY +from myDevices.os.version import OS_VERSION, OS_RASPBIAN_JESSIE, OS_RASPBIAN_WHEEZY BUSLIST = { "I2C": { diff --git a/myDevices/devices/i2c.py b/myDevices/devices/i2c.py index 4be8d1f..ed475bf 100644 --- a/myDevices/devices/i2c.py +++ b/myDevices/devices/i2c.py @@ -14,7 +14,7 @@ import fcntl -from myDevices.utils.version import BOARD_REVISION +from myDevices.os.hardware import BOARD_REVISION from myDevices.devices.bus import Bus # /dev/i2c-X ioctl commands. The ioctl's parameter is always an diff --git a/myDevices/devices/spi.py b/myDevices/devices/spi.py index bab451e..e4a51c0 100644 --- a/myDevices/devices/spi.py +++ b/myDevices/devices/spi.py @@ -17,7 +17,7 @@ import ctypes import struct -from myDevices.utils.version import PYTHON_MAJOR +from sys import version_info from myDevices.devices.bus import Bus # from spi/spidev.h @@ -120,7 +120,7 @@ def __str__(self): def xfer(self, txbuff=None): length = len(txbuff) - if PYTHON_MAJOR >= 3: + if version_info.major >= 3: _txbuff = bytes(txbuff) _txptr = ctypes.create_string_buffer(_txbuff) else: diff --git a/myDevices/os/hardware.py b/myDevices/os/hardware.py index 50c1103..268d4ed 100644 --- a/myDevices/os/hardware.py +++ b/myDevices/os/hardware.py @@ -1,21 +1,45 @@ +""" +This module provides constants for the board revision info and pin mapping as well as +a class for getting hardware info, including manufacturer, model and MAC address. +""" +import re +import sys from uuid import getnode from myDevices.utils.logger import exception, info, warn, error, debug +BOARD_REVISION = 0 +CPU_REVISION = "0" +MAPPING = [] + +try: + with open("/proc/cpuinfo") as f: + rc = re.compile("Revision\s*:\s(.*)\n") + info = f.read() + result = rc.search(info) + if result: + CPU_REVISION = result.group(1) + if CPU_REVISION.startswith("1000"): + CPU_REVISION = CPU_REVISION[-4:] + cpurev = int(CPU_REVISION, 16) + if cpurev < 0x04: + BOARD_REVISION = 1 + MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + elif cpurev < 0x10: + BOARD_REVISION = 2 + MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + else: + BOARD_REVISION = 3 + MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] +except: + exception("Error reading cpuinfo") + + class Hardware: + """Class for getting hardware info, including manufacturer, model and MAC address.""" + def __init__(self): - self.Revision = "0" - try: - with open('/proc/cpuinfo','r') as f: - for line in f: - splitLine = line.split(':') - if len(splitLine) < 2: - continue - key = splitLine[0].strip() - value = splitLine[1].strip() - if key=='Revision': - self.Revision = value - except: - exception ("Error reading cpuinfo") + """Initialize board revision and model dict""" + self.Revision = CPU_REVISION self.model = {} self.model["Beta"] = "Model B (Beta)" self.model["000d"] = self.model["000e"] = self.model["000f"] = self.model["0002"] = self.model["0003"] = self.model["0004"] = self.model["0005"] = self.model["0006"] = "Model B" @@ -27,9 +51,10 @@ def __init__(self): self.model["a01041"] = "Pi 2 Model B" self.model["a21041"] = "Pi 2 Model B" self.model["900092"] = "Zero" - self.model["a22082"]=self.model["a02082"] = "Pi 3 Model B" + self.model["a22082"] = self.model["a02082"] = "Pi 3 Model B" def getManufacturer(self): + """Return manufacturer name as string""" if self.Revision in ["a01041","900092", "a02082", "0012", "0011", "0010", "000e", "0008", "0004"]: return "Sony, UK" if self.Revision == "a21041": @@ -41,6 +66,7 @@ def getManufacturer(self): return "Element14/Premier Farnell" def getModel(self): + """Return model name as string""" try: model = self.model[self.Revision] except: @@ -48,11 +74,12 @@ def getModel(self): return model def getMac(self, format=2): + """Return MAC address as string""" if format < 2: format = 2 if format > 4: format = 4 mac_num = hex(getnode()).replace('0x', '').upper() mac = '-'.join(mac_num[i : i + format] for i in range(0, 11, format)) - #debug("Mac:" + mac) return mac + diff --git a/myDevices/os/version.py b/myDevices/os/version.py new file mode 100644 index 0000000..ee2f31a --- /dev/null +++ b/myDevices/os/version.py @@ -0,0 +1,16 @@ +""" +This module contains code to retrieve the OS version. +""" +OS_VERSION = 0 +OS_RASPBIAN_WHEEZY = 1 +OS_RASPBIAN_JESSIE = 2 + +try: + with open("/etc/apt/sources.list") as f: + sources = f.read() + if "wheezy" in sources: + OS_VERSION = OS_RASPBIAN_WHEEZY + elif "jessie" in sources: + OS_VERSION = OS_RASPBIAN_JESSIE +except: + pass diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 600ec5f..d09e445 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -15,7 +15,7 @@ from myDevices.utils.threadpool import ThreadPool from hashlib import sha1 import urllib.request as req -from myDevices.utils.version import MAPPING +from myDevices.os.hardware import MAPPING from myDevices.devices.bus import checkAllBus, BUSLIST from myDevices.devices.digital.gpio import NativeGPIO as GPIO from myDevices.devices import manager diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py new file mode 100644 index 0000000..595c42d --- /dev/null +++ b/myDevices/test/hardware_test.py @@ -0,0 +1,38 @@ +import unittest +from myDevices.utils.logger import setInfo, info +from myDevices.os.hardware import Hardware, MAPPING, BOARD_REVISION, CPU_REVISION + +class HarwareTest(unittest.TestCase): + def setUp(self): + setInfo() + self.hardware = Hardware() + + def testGetManufacturer(self): + manufacturer = self.hardware.getManufacturer() + info(manufacturer) + self.assertNotEqual(manufacturer, '') + + def testGetModel(self): + model = self.hardware.getModel() + info(model) + self.assertNotEqual(model, 'Unknown') + + def testGetMac(self): + mac = self.hardware.getMac() + info(mac) + self.assertRegex(mac, '^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$') + + def testMapping(self): + info(MAPPING) + self.assertTrue(MAPPING) + + def testBoardRevision(self): + info(BOARD_REVISION) + self.assertNotEqual(BOARD_REVISION, 0) + + def testCpuRevision(self): + info(CPU_REVISION) + self.assertNotEqual(CPU_REVISION, '0') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index 8362888..c7aaf78 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -2,7 +2,6 @@ import os import grp from myDevices.sensors import sensors -from myDevices.utils.version import MAPPING from myDevices.devices import manager from myDevices.utils.config import Config from myDevices.utils import types diff --git a/myDevices/utils/version.py b/myDevices/utils/version.py deleted file mode 100644 index 67a8436..0000000 --- a/myDevices/utils/version.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -import sys - -PYTHON_MAJOR = sys.version_info.major -BOARD_REVISION = 0 - -OS_VERSION = 0 -OS_RASPBIAN_WHEEZY = 1 -OS_RASPBIAN_JESSIE = 2 - -_MAPPING = [[], [], [], []] -_MAPPING[1] = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] -_MAPPING[2] = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] -_MAPPING[3] = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] - -try: - with open("/proc/cpuinfo") as f: - rc = re.compile("Revision\s*:\s(.*)\n") - info = f.read() - result = rc.search(info) - if result != None: - hex_cpurev = result.group(1) - if hex_cpurev.startswith("1000"): - hex_cpurev = hex_cpurev[-4:] - cpurev = int(hex_cpurev, 16) - if cpurev < 0x04: - BOARD_REVISION = 1 - elif cpurev < 0x10: - BOARD_REVISION = 2 - else: - BOARD_REVISION = 3 - -except: - pass - -try: - with open("/etc/apt/sources.list") as f: - sources = f.read() - if "wheezy" in sources: - OS_VERSION = OS_RASPBIAN_WHEEZY - elif "jessie" in sources: - OS_VERSION = OS_RASPBIAN_JESSIE - -except: - pass - -MAPPING = _MAPPING[BOARD_REVISION] From 8d7c40329dd0028009a2b9cd2661237bcefde47b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 15 Jun 2017 18:42:25 -0600 Subject: [PATCH 011/129] Add documentation and clean up raspiconfig module. --- myDevices/cloud/client.py | 2 +- myDevices/os/raspiconfig.py | 38 +++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 2c8e345..1864020 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -924,7 +924,7 @@ def ProcessDeviceCommand(self, messageObject): try: config_id = parameters["id"] arguments = parameters["arguments"] - (retValue, output) = RaspiConfig.Config(config_id, arguments) + (retValue, output) = RaspiConfig.ExecuteConfigCommand(config_id, arguments) data["Output"] = output retValue = str(retValue) except: diff --git a/myDevices/os/raspiconfig.py b/myDevices/os/raspiconfig.py index db0eb54..b32f665 100644 --- a/myDevices/os/raspiconfig.py +++ b/myDevices/os/raspiconfig.py @@ -1,3 +1,6 @@ +""" +This module provices a class for modifying Raspberry Pi configuration settings. +""" from myDevices.utils.logger import exception, info, warn, error, debug from myDevices.os.services import ServiceManager from time import sleep @@ -6,37 +9,50 @@ CUSTOM_CONFIG_SCRIPT = "/etc/myDevices/scripts/config.sh" class RaspiConfig: - def Config(config_id, parameters): - if config_id == 0: - return RaspiConfig.ExpandRootfs() - else: - return RaspiConfig.ExecuteConfigCommand(config_id, parameters) + """Class for modifying configuration settings""" + + @staticmethod def ExpandRootfs(): + """Expand the filesystem""" command = "sudo raspi-config --expand-rootfs" debug('ExpandRootfs command:' + command) (output, returnCode) = ServiceManager.ExecuteCommand(command) debug('ExpandRootfs command:' + command + " retCode: " + returnCode) output = 'reboot required' return (returnCode, output) + + @staticmethod def ExecuteConfigCommand(config_id, parameters): - debug('RaspiConfig::config') + """Execute specified command to modify configuration + + Args: + config_id: Id of command to run + parameters: Parameters to use when executing command + """ + debug('RaspiConfig::ExecuteConfigCommand') + if config_id == 0: + return RaspiConfig.ExpandRootfs() command = "sudo " + CUSTOM_CONFIG_SCRIPT + " " + str(config_id) + " " + str(parameters) (output, returnCode) = ServiceManager.ExecuteCommand(command) debug('ExecuteConfigCommand '+ str(config_id) + ' args: ' + str(parameters) + ' retCode: ' + str(returnCode) + ' output: ' + output ) if "reboot required" in output: ThreadPool.Submit(RaspiConfig.RestartService) - #del output return (returnCode, output) + + @staticmethod def RestartService(): + """Reboot the device""" sleep(5) command = "sudo shutdown -r now" - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = ServiceManager.ExecuteCommand(command) + + @staticmethod def getConfig(): + """Return dict containing configuration settings""" configItem = {} try: (returnCode, output) = RaspiConfig.ExecuteConfigCommand(17, '') if output: - print('output: ' + output) values = output.strip().split(' ') configItem['Camera'] = {} for i in values: @@ -49,19 +65,17 @@ def getConfig(): try: (returnCode, output) = RaspiConfig.ExecuteConfigCommand(10, '') if output: - print('output: ' + output) configItem['DeviceTree'] = int(output.strip()) del output (returnCode, output) = RaspiConfig.ExecuteConfigCommand(18, '') if output: - print('output: ' + output) configItem['Serial'] = int(output.strip()) del output (returnCode, output) = RaspiConfig.ExecuteConfigCommand(20, '') if output: - print('output: ' + output) configItem['OneWire'] = int(output.strip()) del output except: exception('Camera config') + info('RaspiConfig: {}'.format(configItem)) return configItem \ No newline at end of file From 6a38cdd6549d9496c76201dd43c58044b66d0fe6 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 22 Jun 2017 11:49:37 -0600 Subject: [PATCH 012/129] Rename 'os' module to 'system'. --- README.rst | 10 +++++----- myDevices/__main__.py | 2 +- myDevices/cloud/client.py | 6 +++--- myDevices/cloud/updater.py | 2 +- myDevices/devices/bus.py | 2 +- myDevices/devices/i2c.py | 2 +- myDevices/sensors/sensors.py | 6 +++--- myDevices/{os => system}/__init__.py | 0 myDevices/{os => system}/cpu.py | 0 myDevices/{os => system}/hardware.py | 0 myDevices/{os => system}/ipgetter.py | 0 myDevices/{os => system}/raspiconfig.py | 2 +- myDevices/{os => system}/services.py | 0 myDevices/{os => system}/systeminfo.py | 2 +- myDevices/{os => system}/version.py | 0 myDevices/test/hardware_test.py | 2 +- myDevices/test/systeminfo_test.py | 2 +- myDevices/utils/daemon.py | 2 +- myDevices/wifi/WifiManager.py | 2 +- myDevices/wifi/WirelessLib.py | 2 +- setup.py | 2 +- 21 files changed, 23 insertions(+), 23 deletions(-) rename myDevices/{os => system}/__init__.py (100%) rename myDevices/{os => system}/cpu.py (100%) rename myDevices/{os => system}/hardware.py (100%) rename myDevices/{os => system}/ipgetter.py (100%) rename myDevices/{os => system}/raspiconfig.py (98%) rename myDevices/{os => system}/services.py (100%) rename myDevices/{os => system}/systeminfo.py (99%) rename myDevices/{os => system}/version.py (100%) diff --git a/README.rst b/README.rst index db70471..edc0417 100644 --- a/README.rst +++ b/README.rst @@ -164,19 +164,19 @@ System info Information about the device, including CPU, RAM, etc., is currently retrieved via a few different modules. To support a different board you may need to update the agent code for the following items, if applicable: General System Info - General system info, including CPU, RAM, memory, etc. is retrieved via ``myDevices.os.systeminfo.py`` and ``myDevices.os.cpu.py``. These are mostly implemented using cross platform libraries so they may already provide support for your board. If not, they should be modified or overridden to provide the appropriate system info. If your board does not support all the data values currently implemented you can just provide default values where necessary, though this may affect the data display in the Cayenne dashboard. + General system info, including CPU, RAM, memory, etc. is retrieved via ``myDevices.system.systeminfo.py`` and ``myDevices.system.cpu.py``. These are mostly implemented using cross platform libraries so they may already provide support for your board. If not, they should be modified or overridden to provide the appropriate system info. If your board does not support all the data values currently implemented you can just provide default values where necessary, though this may affect the data display in the Cayenne dashboard. Hardware Info - Hardware info, including make, model, etc. is retrieved via ``myDevices.os.hardware.py``. This should be modified or overridden to provide the appropriate hardware info for your board. + Hardware info, including make, model, etc. is retrieved via ``myDevices.system.hardware.py``. This should be modified or overridden to provide the appropriate hardware info for your board. Pin Mapping - The mapping of the on-board pins is provided in ``myDevices.os.hardware.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab. + The mapping of the on-board pins is provided in ``myDevices.system.hardware.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab. Settings -------- -Currently the Raspberry Pi agent has settings for enabling/disabling the device tree, SPI, I²C, serial and camera. These are set via the ``myDevices.os.raspiconfig`` module which runs a separate Bash script at ``/etc/myDevices/scripts/config.sh``. If any of these settings are available on your board and you would like to support them you can override or replace ``myDevices.os.raspiconfig.py``. Otherwise the settings functionality can be ignored. +Currently the Raspberry Pi agent has settings for enabling/disabling the device tree, SPI, I²C, serial and camera. These are set via the ``myDevices.system.raspiconfig`` module which runs a separate Bash script at ``/etc/myDevices/scripts/config.sh``. If any of these settings are available on your board and you would like to support them you can override or replace ``myDevices.system.raspiconfig.py``. Otherwise the settings functionality can be ignored. -*Note:* For security reasons the Cayenne agent is designed to be able to run from an account without root privileges. If any of your I/O, system info or settings code requires root access consider running it via a separate process that can be launched using sudo. For example, the ``myDevices.os.raspiconfig`` module uses this method to update config settings. +*Note:* For security reasons the Cayenne agent is designed to be able to run from an account without root privileges. If any of your I/O, system info or settings code requires root access consider running it via a separate process that can be launched using sudo. For example, the ``myDevices.system.raspiconfig`` module uses this method to update config settings. ************ Contributing diff --git a/myDevices/__main__.py b/myDevices/__main__.py index 0f8c8b5..023057a 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -9,7 +9,7 @@ from myDevices.utils.logger import exception, setDebug, info, debug, error, logToFile, setInfo from signal import signal, SIGUSR1, SIGINT from resource import getrlimit, setrlimit, RLIMIT_AS -from myDevices.os.services import ProcessInfo +from myDevices.system.services import ProcessInfo from myDevices.utils.daemon import Daemon def setMemoryLimit(rsrc, megs=200): diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 1864020..ddfeee5 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -14,14 +14,14 @@ from enum import Enum, unique from myDevices.utils.config import Config from myDevices.utils.logger import exception, info, warn, error, debug, logJson -from myDevices.os import services, ipgetter +from myDevices.system import services, ipgetter from myDevices.sensors import sensors -from myDevices.os.hardware import Hardware +from myDevices.system.hardware import Hardware from myDevices.wifi import WifiManager from myDevices.cloud.scheduler import SchedulerEngine from myDevices.cloud.download_speed import DownloadSpeed from myDevices.cloud.updater import Updater -from myDevices.os.raspiconfig import RaspiConfig +from myDevices.system.raspiconfig import RaspiConfig from myDevices.utils.daemon import Daemon from myDevices.utils.threadpool import ThreadPool from myDevices.utils.history import History diff --git a/myDevices/cloud/updater.py b/myDevices/cloud/updater.py index 932ac0c..81a6d14 100644 --- a/myDevices/cloud/updater.py +++ b/myDevices/cloud/updater.py @@ -1,7 +1,7 @@ from myDevices.utils.logger import exception, info, warn, error, debug, setDebug from time import time, sleep from sched import scheduler -from myDevices.os import services +from myDevices.system import services from distutils.version import LooseVersion, StrictVersion from os import mkdir, path from threading import Thread diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index 383d746..be1cba7 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -17,7 +17,7 @@ import subprocess from myDevices.utils.logger import debug, info -from myDevices.os.version import OS_VERSION, OS_RASPBIAN_JESSIE, OS_RASPBIAN_WHEEZY +from myDevices.system.version import OS_VERSION, OS_RASPBIAN_JESSIE, OS_RASPBIAN_WHEEZY BUSLIST = { "I2C": { diff --git a/myDevices/devices/i2c.py b/myDevices/devices/i2c.py index ed475bf..8bfb1e6 100644 --- a/myDevices/devices/i2c.py +++ b/myDevices/devices/i2c.py @@ -14,7 +14,7 @@ import fcntl -from myDevices.os.hardware import BOARD_REVISION +from myDevices.system.hardware import BOARD_REVISION from myDevices.devices.bus import Bus # /dev/i2c-X ioctl commands. The ioctl's parameter is always an diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index d09e445..443a6ef 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -6,7 +6,7 @@ from time import sleep, time from json import loads, dumps from threading import Thread, RLock -from myDevices.os import services +from myDevices.system import services from datetime import datetime, timedelta from os import path, getpid from urllib import parse @@ -15,13 +15,13 @@ from myDevices.utils.threadpool import ThreadPool from hashlib import sha1 import urllib.request as req -from myDevices.os.hardware import MAPPING +from myDevices.system.hardware import MAPPING from myDevices.devices.bus import checkAllBus, BUSLIST from myDevices.devices.digital.gpio import NativeGPIO as GPIO from myDevices.devices import manager from myDevices.devices import instance from myDevices.utils.types import M_JSON -from myDevices.os.systeminfo import SystemInfo +from myDevices.system.systeminfo import SystemInfo REFRESH_FREQUENCY = 0.10 #seconds SENSOR_INFO_SLEEP = 0.05 diff --git a/myDevices/os/__init__.py b/myDevices/system/__init__.py similarity index 100% rename from myDevices/os/__init__.py rename to myDevices/system/__init__.py diff --git a/myDevices/os/cpu.py b/myDevices/system/cpu.py similarity index 100% rename from myDevices/os/cpu.py rename to myDevices/system/cpu.py diff --git a/myDevices/os/hardware.py b/myDevices/system/hardware.py similarity index 100% rename from myDevices/os/hardware.py rename to myDevices/system/hardware.py diff --git a/myDevices/os/ipgetter.py b/myDevices/system/ipgetter.py similarity index 100% rename from myDevices/os/ipgetter.py rename to myDevices/system/ipgetter.py diff --git a/myDevices/os/raspiconfig.py b/myDevices/system/raspiconfig.py similarity index 98% rename from myDevices/os/raspiconfig.py rename to myDevices/system/raspiconfig.py index b32f665..c4489ea 100644 --- a/myDevices/os/raspiconfig.py +++ b/myDevices/system/raspiconfig.py @@ -2,7 +2,7 @@ This module provices a class for modifying Raspberry Pi configuration settings. """ from myDevices.utils.logger import exception, info, warn, error, debug -from myDevices.os.services import ServiceManager +from myDevices.system.services import ServiceManager from time import sleep from myDevices.utils.threadpool import ThreadPool diff --git a/myDevices/os/services.py b/myDevices/system/services.py similarity index 100% rename from myDevices/os/services.py rename to myDevices/system/services.py diff --git a/myDevices/os/systeminfo.py b/myDevices/system/systeminfo.py similarity index 99% rename from myDevices/os/systeminfo.py rename to myDevices/system/systeminfo.py index 9823836..c450bc9 100644 --- a/myDevices/os/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -5,7 +5,7 @@ import psutil import netifaces from myDevices.utils.logger import exception -from myDevices.os.cpu import CpuInfo +from myDevices.system.cpu import CpuInfo class SystemInfo(): diff --git a/myDevices/os/version.py b/myDevices/system/version.py similarity index 100% rename from myDevices/os/version.py rename to myDevices/system/version.py diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py index 595c42d..656f1ec 100644 --- a/myDevices/test/hardware_test.py +++ b/myDevices/test/hardware_test.py @@ -1,6 +1,6 @@ import unittest from myDevices.utils.logger import setInfo, info -from myDevices.os.hardware import Hardware, MAPPING, BOARD_REVISION, CPU_REVISION +from myDevices.system.hardware import Hardware, MAPPING, BOARD_REVISION, CPU_REVISION class HarwareTest(unittest.TestCase): def setUp(self): diff --git a/myDevices/test/systeminfo_test.py b/myDevices/test/systeminfo_test.py index 3225ce2..8769946 100644 --- a/myDevices/test/systeminfo_test.py +++ b/myDevices/test/systeminfo_test.py @@ -1,5 +1,5 @@ import unittest -from myDevices.os.systeminfo import SystemInfo +from myDevices.system.systeminfo import SystemInfo from myDevices.utils.logger import setInfo, info diff --git a/myDevices/utils/daemon.py b/myDevices/utils/daemon.py index 5e4ca22..ce9ad50 100644 --- a/myDevices/utils/daemon.py +++ b/myDevices/utils/daemon.py @@ -4,7 +4,7 @@ from sys import exit from datetime import datetime from myDevices.utils.logger import exception, info, warn, error, debug -from myDevices.os.services import ServiceManager +from myDevices.system.services import ServiceManager #defining reset timeout in seconds RESET_TIMEOUT = 30 diff --git a/myDevices/wifi/WifiManager.py b/myDevices/wifi/WifiManager.py index 1d1e5e3..5980eef 100644 --- a/myDevices/wifi/WifiManager.py +++ b/myDevices/wifi/WifiManager.py @@ -1,7 +1,7 @@ from myDevices.wifi.WirelessLib import Wireless from json import dumps, loads, JSONEncoder, JSONDecoder from myDevices.utils.logger import exception, info, warn, error, debug -from myDevices.os.services import ServiceManager +from myDevices.system.services import ServiceManager class Network(): def GetNetworkId(): diff --git a/myDevices/wifi/WirelessLib.py b/myDevices/wifi/WirelessLib.py index 42af226..c42163f 100644 --- a/myDevices/wifi/WirelessLib.py +++ b/myDevices/wifi/WirelessLib.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod import subprocess from time import sleep -from myDevices.os.services import ServiceManager +from myDevices.system.services import ServiceManager # send a command to the shell and return the result def cmd(cmd): diff --git a/setup.py b/setup.py index d471551..bea428e 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ keywords = 'myDevices Cayenne IoT', url = 'https://www.mydevices.com/', classifiers = classifiers, - packages = ["myDevices", "myDevices.cloud", "myDevices.utils", "myDevices.os", "myDevices.sensors" , "myDevices.wifi", "myDevices.schedule", "myDevices.requests_futures", "myDevices.devices", "myDevices.devices.analog", "myDevices.devices.digital", "myDevices.devices.sensor", "myDevices.devices.shield", "myDevices.decorators"], + packages = ["myDevices", "myDevices.cloud", "myDevices.utils", "myDevices.system", "myDevices.sensors" , "myDevices.wifi", "myDevices.schedule", "myDevices.requests_futures", "myDevices.devices", "myDevices.devices.analog", "myDevices.devices.digital", "myDevices.devices.sensor", "myDevices.devices.shield", "myDevices.decorators"], install_requires = ['enum34', 'iwlib', 'jsonpickle', 'netifaces', 'psutil >= 0.7.0', 'requests'], data_files = [('/etc/myDevices/scripts', ['scripts/config.sh'])] ) From 23c7680528cdeef0d3330618f435b09e05ec22e1 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 22 Jun 2017 12:42:41 -0600 Subject: [PATCH 013/129] Clean up services code. --- myDevices/__main__.py | 2 +- myDevices/system/raspiconfig.py | 2 +- myDevices/system/services.py | 112 +++++++++++++++----------------- 3 files changed, 55 insertions(+), 61 deletions(-) diff --git a/myDevices/__main__.py b/myDevices/__main__.py index 023057a..b0f4e54 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -16,7 +16,7 @@ def setMemoryLimit(rsrc, megs=200): """Set the memory usage limit for the agent process""" size = megs * 1048576 soft, hard = getrlimit(rsrc) - setrlimit(rsrc, (size, hard)) #limit to one kilobyte + setrlimit(rsrc, (size, hard)) soft, hard = getrlimit(rsrc) try: diff --git a/myDevices/system/raspiconfig.py b/myDevices/system/raspiconfig.py index c4489ea..7f436a2 100644 --- a/myDevices/system/raspiconfig.py +++ b/myDevices/system/raspiconfig.py @@ -1,5 +1,5 @@ """ -This module provices a class for modifying Raspberry Pi configuration settings. +This module provides a class for modifying Raspberry Pi configuration settings. """ from myDevices.utils.logger import exception, info, warn, error, debug from myDevices.system.services import ServiceManager diff --git a/myDevices/system/services.py b/myDevices/system/services.py index e68bfe4..0c08430 100644 --- a/myDevices/system/services.py +++ b/myDevices/system/services.py @@ -1,44 +1,25 @@ +""" +This module provides classes for retrieving process and service info, as well as managing processes and services. +""" from subprocess import Popen, PIPE from enum import Enum, unique -from myDevices.utils.logger import exception, info, warn, error, debug -from psutil import Process, process_iter, virtual_memory, cpu_percent from threading import RLock +from psutil import Process, process_iter, virtual_memory, cpu_percent +from myDevices.utils.logger import exception, info, warn, error, debug class ProcessInfo: + """Class for getting process info and killing processes""" + def __init__(self): + """Initialize process information""" self.Name = None - self.Pid = None - self.Username = None + self.Pid = None + self.Username = None self.Cmdline = None - # self.Cwd = None - # self.Process = None - # self.CpuPercent = None - # self.Exe = None - # self.Status = None - # self.CreateTime = None - # self.MemoryPercent = None - # self.NumThreads = None - # self.NumFds = None - def Suspend(self): - debug('ProcessManager::Suspend Name:' + self.Name + ' PID:' + str(self.Pid)) - try: - process = Process(self.Pid) - process.suspend() - except Exception as ex: - error('ProcessInfo::Suspend failed Name:' + self.Name + ' PID:' + str(self.Pid) + ' Exception:' + str(ex)) - return False - return True - def Resume(self): - debug('ProcessManager::Resume Name:' + self.Name + ' PID:' + str(self.Pid)) - try: - process = Process(self.Pid) - process.resume() - except Exception as ex: - error('ProcessInfo::Resume failed Name:' + self.Name + ' PID:' + str(self.Pid) + ' Exception:' + str(ex)) - return False - return True + def Terminate(self): - print('ProcessManager::Terminate Name:' + self.Name + ' PID:' + str(self.Pid)) + """Terminate the process""" + info('ProcessManager::Terminate Name:' + self.Name + ' PID:' + str(self.Pid)) try: process = Process(self.Pid) process.terminate() @@ -46,16 +27,10 @@ def Terminate(self): error('ProcessInfo::Terminate failed Name:' + self.Name + ' PID:' + str(self.Pid) + ' Exception:' + str(ex)) return False return True - def Wait(self, waitTimeout): - debug('ProcessManager::Wait Name:' + self.Name + ' PID:' + str(self.Pid)) - try: - process = Process(self.Pid) - process.wait(timeout = waitTimeout) - except Exception as ex: - error('ProcessInfo::Wait failed Name:' + self.Name + ' PID:' + str(self.Pid) + ' Exception:' + str(ex)) - return False - return True + + @staticmethod def IsRunning(pid): + """Return True if process with specified pid is running""" try: process = Process(pid) except Exception as ex: @@ -64,7 +39,10 @@ def IsRunning(pid): return True class ProcessManager: + """Class for retrieving running processes and processor usage info""" + def __init__(self): + """Initialize process and processor info""" debug('') self.mapProcesses = {} self.VisibleMemory = 0 @@ -77,7 +55,9 @@ def __init__(self): self.totalMemoryCount = 0 self.totalProcessorCount = 0 self.mutex = RLock() + def Run(self): + """Get running process info""" debug('') try: running_processes = [] @@ -91,18 +71,8 @@ def Run(self): processInfo.Name = p.name() if callable(p.name) else p.name processInfo.Username = p.username() if callable(p.username) else p.username processInfo.Cmdline = p.cmdline() if callable(p.cmdline) else p.cmdline - # processInfo.Cwd = p.getcwd() - # processInfo.Process = p - # processInfo.CpuPercent = p.get_cpu_percent(interval=0.1) - # processInfo.Exe = p.exe - # processInfo.Status = p.status - # processInfo.CreateTime = p.create_time - # processInfo.MemoryPercent = p.get_memory_percent() - # processInfo.NumThreads = p.get_num_threads() - # processInfo.NumFds = p.get_num_fds() self.mapProcesses[p.pid] = processInfo - except Exception as e: - # debug('ProcessManager::Run Exception {} on pid {}'.format(e, pid)) + except Exception: pass remove = [key for key in self.mapProcesses.keys() if key not in running_processes] for key in remove: @@ -111,7 +81,9 @@ def Run(self): except: exception('ProcessManager::Run failed') debug('ProcessManager::Run retrieved {} processes'.format(len(self.mapProcesses))) + def GetProcessList(self): + """Return list of running processes""" process_list = [] with self.mutex: for key, value in self.mapProcesses.items(): @@ -125,7 +97,9 @@ def GetProcessList(self): process['pid'] = value.Pid process_list.append(process) return process_list + def KillProcess(self, pid): + """Kill the process specified by pid""" retVal = False with self.mutex: process = self.mapProcesses.get(pid) @@ -138,7 +112,9 @@ def KillProcess(self, pid): debug('KillProcess: {}'.format(e)) pass return retVal + def RefreshProcessManager(self): + """Refresh processor usage and memory info""" try: if self.VisibleMemory: del self.VisibleMemory @@ -164,20 +140,25 @@ def RefreshProcessManager(self): self.PeakMemoryUsage = self.AvailableMemory except: exception('ProcessManager::RefreshProcessManager failed') - + @unique class ServiceState(Enum): - Unkown = 0 + Unknown = 0 Running = 1 NotRunning = 2 NotAvailable = 3 - + class ServiceManager: + """Class for retrieving service info and managing services""" + def __init__(self): + """Initialize service info""" self.Init = True self.mapServices = {} self.mutex = RLock() + def Run(self): + """Get info about services""" debug('ServiceManager::Run') with self.mutex: (output, returnCode) = ServiceManager.ExecuteCommand("service --status-all") @@ -201,46 +182,59 @@ def Run(self): del self.mapServices[key] debug('ServiceManager::Run retrieved ' + str(len(self.mapServices)) + ' services') del output + def GetServiceList(self): + """Return list of services""" service_list = [] with self.mutex: for key, value in self.mapServices.items(): process = {} - process['ProcessName'] = str(key) + process['ProcessName'] = str(key) process['ProcessDescription'] = str(value) - process['CompanyName'] = str(key) + process['CompanyName'] = str(key) service_list.append(process) return service_list + def Start(self, serviceName): + """Start the named service""" debug('ServiceManager::Start') command = "sudo service " + serviceName + " start" (output, returnCode) = ServiceManager.ExecuteCommand(command) debug('ServiceManager::Start command:' + command + " output: " + output) del output return returnCode + def Stop(self, serviceName): + """Stop the named service""" debug('ServiceManager::Stop') command = "sudo service " + serviceName + " stop" (output, returnCode) = ServiceManager.ExecuteCommand(command) debug('ServiceManager::Stop command:' + command + " output: " + output) del output return returnCode + def Status(self, serviceName): + """Get the status of the named service""" debug('ServiceManager::Status') command = "service " + serviceName + " status" (output, returnCode) = ServiceManager.ExecuteCommand(command) debug('ServiceManager::Stop command:' + command + " output: " + output) del output return returnCode + + @staticmethod def SetMemoryLimits(): + """Set memory limit when launching a process to the default maximum""" try: from resource import getrlimit, setrlimit, RLIMIT_AS soft, hard = getrlimit(RLIMIT_AS) setrlimit(RLIMIT_AS, (hard, hard)) - soft, hard = getrlimit(RLIMIT_AS) except: pass + + @staticmethod def ExecuteCommand(command, increaseMemoryLimit=False): + """Execute a specified command, increasing the processes memory limits if specified""" debug('ServiceManager::ExecuteCommand: ' + command) output = "" returncode = 1 @@ -265,4 +259,4 @@ def ExecuteCommand(command, increaseMemoryLimit=False): debug('ServiceManager::ExecuteCommand: ' + command + ' ' + str(output)) retOut = str(output) del output - return (retOut, returncode) \ No newline at end of file + return (retOut, returncode) From 9dbcacc5b2e9f998ee946912e90378764a039a37 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 22 Jun 2017 13:27:12 -0600 Subject: [PATCH 014/129] Skip re-getting memory limit. --- myDevices/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/myDevices/__main__.py b/myDevices/__main__.py index b0f4e54..c8f645b 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -17,7 +17,6 @@ def setMemoryLimit(rsrc, megs=200): size = megs * 1048576 soft, hard = getrlimit(rsrc) setrlimit(rsrc, (size, hard)) - soft, hard = getrlimit(rsrc) try: #Only set memory limit on 32-bit systems From 068fbf00cced40eb9bba54d1002ef59a820ab071 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 22 Jun 2017 15:46:05 -0600 Subject: [PATCH 015/129] Skip camera settings check if return value is invalid. --- myDevices/system/raspiconfig.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/myDevices/system/raspiconfig.py b/myDevices/system/raspiconfig.py index 7f436a2..2495c35 100644 --- a/myDevices/system/raspiconfig.py +++ b/myDevices/system/raspiconfig.py @@ -56,8 +56,9 @@ def getConfig(): values = output.strip().split(' ') configItem['Camera'] = {} for i in values: - val1 = i.split('=') - configItem['Camera'][val1[0]] = int(val1[1]) + if '=' in i: + val1 = i.split('=') + configItem['Camera'][val1[0]] = int(val1[1]) del output except: exception('Camera config') From 129fab607dc66f9759494abbfd9931f2e6447264 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 22 Jun 2017 15:46:26 -0600 Subject: [PATCH 016/129] Add check for 'stretch' OS. --- myDevices/devices/bus.py | 4 ++-- myDevices/system/version.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index be1cba7..23f91f7 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -17,7 +17,7 @@ import subprocess from myDevices.utils.logger import debug, info -from myDevices.system.version import OS_VERSION, OS_RASPBIAN_JESSIE, OS_RASPBIAN_WHEEZY +from myDevices.system.version import OS_VERSION, OS_JESSIE, OS_WHEEZY BUSLIST = { "I2C": { @@ -29,7 +29,7 @@ "SPI": { "enabled": False, "gpio": {7:"CE1", 8:"CE0", 9:"MISO", 10:"MOSI", 11:"SCLK"}, - "modules": ["spi-bcm2708" if OS_VERSION == OS_RASPBIAN_WHEEZY else "spi-bcm2835"] + "modules": ["spi-bcm2708" if OS_VERSION == OS_WHEEZY else "spi-bcm2835"] }, "UART": { diff --git a/myDevices/system/version.py b/myDevices/system/version.py index ee2f31a..8f895c8 100644 --- a/myDevices/system/version.py +++ b/myDevices/system/version.py @@ -2,15 +2,18 @@ This module contains code to retrieve the OS version. """ OS_VERSION = 0 -OS_RASPBIAN_WHEEZY = 1 -OS_RASPBIAN_JESSIE = 2 +OS_WHEEZY = 1 +OS_JESSIE = 2 +OS_STRETCH = 3 try: with open("/etc/apt/sources.list") as f: sources = f.read() if "wheezy" in sources: - OS_VERSION = OS_RASPBIAN_WHEEZY + OS_VERSION = OS_WHEEZY elif "jessie" in sources: - OS_VERSION = OS_RASPBIAN_JESSIE + OS_VERSION = OS_JESSIE + elif "stretch" in sources: + OS_VERSION = OS_STRETCH except: pass From 321467bf8cd7b674f242127b00b6aa2258c15b7a Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 23 Jun 2017 15:59:49 -0600 Subject: [PATCH 017/129] Add Tinker Board device info. --- myDevices/system/hardware.py | 24 +++++++++++++++--------- myDevices/test/hardware_test.py | 3 ++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index 268d4ed..8f2a747 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -20,16 +20,19 @@ CPU_REVISION = result.group(1) if CPU_REVISION.startswith("1000"): CPU_REVISION = CPU_REVISION[-4:] - cpurev = int(CPU_REVISION, 16) - if cpurev < 0x04: - BOARD_REVISION = 1 - MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] - elif cpurev < 0x10: - BOARD_REVISION = 2 - MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + if CPU_REVISION == "0000": + MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] else: - BOARD_REVISION = 3 - MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] + cpurev = int(CPU_REVISION, 16) + if cpurev < 0x04: + BOARD_REVISION = 1 + MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + elif cpurev < 0x10: + BOARD_REVISION = 2 + MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + else: + BOARD_REVISION = 3 + MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] except: exception("Error reading cpuinfo") @@ -52,6 +55,7 @@ def __init__(self): self.model["a21041"] = "Pi 2 Model B" self.model["900092"] = "Zero" self.model["a22082"] = self.model["a02082"] = "Pi 3 Model B" + self.model["0000"] = "Tinker Board" def getManufacturer(self): """Return manufacturer name as string""" @@ -63,6 +67,8 @@ def getManufacturer(self): return "Qisda" if self.Revision in ["0006", "0007", "000d"]: return "Egoman" + if self.Revision == "0000": + return "ASUS" return "Element14/Premier Farnell" def getModel(self): diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py index 656f1ec..ff8db9c 100644 --- a/myDevices/test/hardware_test.py +++ b/myDevices/test/hardware_test.py @@ -28,7 +28,8 @@ def testMapping(self): def testBoardRevision(self): info(BOARD_REVISION) - self.assertNotEqual(BOARD_REVISION, 0) + self.assertGreaterEqual(BOARD_REVISION, 0) + self.assertLessEqual(BOARD_REVISION, 3) def testCpuRevision(self): info(CPU_REVISION) From 5c8210d079c30814a6ab052e107a813337820267 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 23 Jun 2017 17:35:57 -0600 Subject: [PATCH 018/129] Add support for getting Tinker Board GPIO info. --- myDevices/devices/digital/__init__.py | 4 +- myDevices/devices/digital/gpio.py | 100 +++++++++++++++----------- myDevices/sensors/sensors.py | 13 ++-- 3 files changed, 67 insertions(+), 50 deletions(-) diff --git a/myDevices/devices/digital/__init__.py b/myDevices/devices/digital/__init__.py index 6d0d4fd..96568db 100644 --- a/myDevices/devices/digital/__init__.py +++ b/myDevices/devices/digital/__init__.py @@ -47,13 +47,13 @@ def __getFunction__(self, channel): def __setFunction__(self, channel, func): raise NotImplementedError - def __digitalRead__(self, chanel): + def __digitalRead__(self, channel): raise NotImplementedError def __portRead__(self): raise NotImplementedError - def __digitalWrite__(self, chanel, value): + def __digitalWrite__(self, channel, value): raise NotImplementedError def __portWrite__(self, value): diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 3a68dfc..3420938 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -16,61 +16,69 @@ import mmap from time import sleep from myDevices.utils.types import M_JSON -from myDevices.utils.logger import debug, info, error +from myDevices.utils.logger import debug, info, error, exception from myDevices.devices.digital import GPIOPort from myDevices.decorators.rest import request, response +from myDevices.system.hardware import MAPPING +try: + import ASUS.GPIO as gpio_library +except: + gpio_library = None FSEL_OFFSET = 0 # 0x0000 PINLEVEL_OFFSET = 13 # 0x0034 / 4 BLOCK_SIZE = (4*1024) -GPIO_COUNT = 54 FUNCTIONS = ["IN", "OUT", "ALT5", "ALT4", "ALT0", "ALT1", "ALT2", "ALT3", "PWM"] class NativeGPIO(GPIOPort): - GPIO_COUNT = 54 + IN = 0 + OUT = 1 + ALT5 = 2 + ALT4 = 3 + ALT0 = 4 + ALT1 = 5 + ALT2 = 6 + ALT3 = 7 + PWM = 8 - IN = 0 - OUT = 1 - ALT5 = 2 - ALT4 = 3 - ALT0 = 4 - ALT1 = 5 - ALT2 = 6 - ALT3 = 7 - PWM = 8 + ASUS_GPIO = 44 - LOW = 0 - HIGH = 1 + LOW = 0 + HIGH = 1 - PUD_OFF = 0 - PUD_DOWN = 1 - PUD_UP = 2 + PUD_OFF = 0 + PUD_DOWN = 1 + PUD_UP = 2 - RATIO = 1 - ANGLE = 2 - instance = None + RATIO = 1 + ANGLE = 2 + instance = None def __init__(self): if not NativeGPIO.instance: - GPIOPort.__init__(self, 54) - self.export = range(54) + self.pins = [pin for pin in MAPPING if type(pin) is int] + GPIOPort.__init__(self, max(self.pins) + 1) self.post_value = True self.post_function = True self.gpio_setup = [] self.gpio_reset = [] - self.valueFile = [0 for i in range(54)] - self.functionFile = [0 for i in range(54)] - for i in range(54): + self.gpio_map = None + self.valueFile = {pin:0 for pin in self.pins} + self.functionFile = {pin:0 for pin in self.pins} + for pin in self.pins: # Export the pins here to prevent a delay when accessing the values for the # first time while waiting for the file group to be set - self.__checkFilesystemExport__(i) - try: - with open('/dev/gpiomem', 'rb') as gpiomem: - self.gpio_map = mmap.mmap(gpiomem.fileno(), BLOCK_SIZE, prot=mmap.PROT_READ) - except OSError as err: - error(err) + self.__checkFilesystemExport__(pin) + if gpio_library: + gpio_library.setmode(gpio_library.ASUS) + else: + try: + with open('/dev/gpiomem', 'rb') as gpiomem: + self.gpio_map = mmap.mmap(gpiomem.fileno(), BLOCK_SIZE, prot=mmap.PROT_READ) + except OSError as err: + error(err) NativeGPIO.instance = self def __del__(self): @@ -139,7 +147,7 @@ def close(self): self.__digitalWrite__(gpio, g["value"]) def checkDigitalChannelExported(self, channel): - if not channel in self.export: + if not channel in self.pins: raise Exception("Channel %d is not allowed" % channel) def checkPostingFunctionAllowed(self): @@ -235,14 +243,20 @@ def __getFunction__(self, channel): self.__checkFilesystemFunction__(channel) self.checkDigitalChannelExported(channel) try: + if gpio_library: + value = gpio_library.gpio_function(channel) + # If this is not a GPIO function return it, otherwise check the function file to see + # if it is an IN or OUT pin since the ASUS library doesn't provide that info. + if value != self.ASUS_GPIO: + return value r = self.functionFile[channel].read() self.functionFile[channel].seek(0) - if (r.startswith("out")): + if r.startswith("out"): return self.OUT else: return self.IN except Exception as ex: - #error('Failed on __getFunction__: '+ str(channel) + ' ' + str(ex)) + # error('Failed on __getFunction__: '+ str(channel) + ' ' + str(ex)) return -1 def __setFunction__(self, channel, value): @@ -261,13 +275,13 @@ def __setFunction__(self, channel, value): def __portRead__(self): value = 0 - for i in self.export: + for i in self.pins: value |= self.__digitalRead__(i) << i return value def __portWrite__(self, value): - if len(self.export) < 54: - for i in self.export: + if len(self.pins) <= value.bit_length(): + for i in self.pins: if self.getFunction(i) == self.OUT: self.__digitalWrite__(i, (value >> i) & 1) else: @@ -284,7 +298,7 @@ def wildcard(self, compact=False): v = "value" values = {} - for i in self.export: + for i in self.pins: if compact: func = self.getFunction(i) else: @@ -297,10 +311,12 @@ def getFunction(self, channel): def getFunctionString(self, channel): f = self.getFunction(channel) - try: - function_string = FUNCTIONS[f] - except: - function_string = 'UNKNOWN' + function_string = 'UNKNOWN' + if f >= 0: + try: + function_string = FUNCTIONS[f] + except: + pass return function_string def input(self, channel): diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 443a6ef..fb143a1 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -270,12 +270,13 @@ def BusInfo(self): json = {} for (bus, value) in BUSLIST.items(): json[bus] = int(value["enabled"]) - gpios = {} - for gpio in range(GPIO.GPIO_COUNT): - gpios[gpio] = {} - gpios[gpio]['function'] = self.gpio.getFunctionString(gpio) - gpios[gpio]['value'] = int(self.gpio.input(gpio)) - json['GPIO'] = gpios + pin_states = {} + pins = [pin for pin in MAPPING if type(pin) is int] + for pin in pins: + pin_states[pin] = {} + pin_states[pin]['function'] = self.gpio.getFunctionString(pin) + pin_states[pin]['value'] = int(self.gpio.input(pin)) + json['GPIO'] = pin_states json['GpioMap'] = MAPPING self.currentBusInfo = json return self.currentBusInfo From fc7e86617fd06bd71b7d09bc7f8e72050fbf7253 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 26 Jun 2017 15:36:48 -0600 Subject: [PATCH 019/129] Use ASUS.GPIO library for getting pin info, use writevalue script for writing to root GPIO files. --- myDevices/devices/digital/ds2408.py | 2 +- myDevices/devices/digital/gpio.py | 59 ++++++++++++++++++++--------- myDevices/devices/writevalue.py | 25 ++++++++++-- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/myDevices/devices/digital/ds2408.py b/myDevices/devices/digital/ds2408.py index e539dab..37a1a83 100644 --- a/myDevices/devices/digital/ds2408.py +++ b/myDevices/devices/digital/ds2408.py @@ -78,7 +78,7 @@ def readByte(self): def writeByte(self, value): try: info('DS2408 writeByte {} {} {}'.format(self.slave, value, bytearray([value]))) - command = 'sudo python3 -m myDevices.devices.writevalue /sys/bus/w1/devices/{}/output {}'.format(self.slave, value) + command = 'sudo python3 -m myDevices.devices.writevalue -b -f /sys/bus/w1/devices/{}/output -v {}'.format(self.slave, value) subprocess.call(command.split()) except Exception as ex: error('DS2408 writeByte error: {}'.format(ex)) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 3420938..6db1f4b 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -14,6 +14,7 @@ import os import mmap +import subprocess from time import sleep from myDevices.utils.types import M_JSON from myDevices.utils.logger import debug, info, error, exception @@ -54,6 +55,9 @@ class NativeGPIO(GPIOPort): RATIO = 1 ANGLE = 2 + + DIRECTION_FILE = "/sys/class/gpio/gpio%s/direction" + instance = None def __init__(self): @@ -65,8 +69,8 @@ def __init__(self): self.gpio_setup = [] self.gpio_reset = [] self.gpio_map = None - self.valueFile = {pin:0 for pin in self.pins} - self.functionFile = {pin:0 for pin in self.pins} + self.valueFile = {pin:None for pin in self.pins} + self.functionFile = {pin:None for pin in self.pins} for pin in self.pins: # Export the pins here to prevent a delay when accessing the values for the # first time while waiting for the file group to be set @@ -84,6 +88,12 @@ def __init__(self): def __del__(self): if self.gpio_map: self.gpio_map.close() + for value in self.valueFile.values(): + if value: + value.close() + for value in self.functionFile.values(): + if value: + value.close() class SetupException(BaseException): pass @@ -157,6 +167,12 @@ def checkPostingFunctionAllowed(self): def checkPostingValueAllowed(self): if not self.post_value: raise ValueError("POSTing value to native GPIO not allowed") + + def __getFunctionFilePath__(self, channel): + return "/sys/class/gpio/gpio%s/direction" % channel + + def __getValueFilePath__(self, channel): + return "/sys/class/gpio/gpio%s/value" % channel def __checkFilesystemExport__(self, channel): #debug("checkExport for channel %d" % channel) @@ -166,19 +182,19 @@ def __checkFilesystemExport__(self, channel): with open("/sys/class/gpio/export", "a") as f: f.write("%s" % channel) except Exception as ex: - #error('Failed on __checkFilesystemExport__: ' + str(channel) + ' ' + str(ex)) + error('Failed on __checkFilesystemExport__: ' + str(channel) + ' ' + str(ex)) return False return True def __checkFilesystemFunction__(self, channel): - if self.functionFile[channel] == 0: + if not self.functionFile[channel]: #debug("function file %d not open" %channel) valRet = self.__checkFilesystemExport__(channel) if not valRet: return for i in range(10): try: - self.functionFile[channel] = open("/sys/class/gpio/gpio%s/direction" % channel, "w+") + self.functionFile[channel] = open(self.__getFunctionFilePath__(channel), "w+") break except PermissionError: # Try again since the file group might not have been set to the gpio group @@ -187,14 +203,14 @@ def __checkFilesystemFunction__(self, channel): def __checkFilesystemValue__(self, channel): - if self.valueFile[channel] == 0: + if not self.valueFile[channel]: #debug("value file %d not open" %channel) valRet = self.__checkFilesystemExport__(channel) if not valRet: return for i in range(10): try: - self.valueFile[channel] = open("/sys/class/gpio/gpio%s/value" % channel, "w+") + self.valueFile[channel] = open(self.__getValueFilePath__(channel), "w+") break except PermissionError: # Try again since the file group might not have been set to the gpio group @@ -220,12 +236,16 @@ def __digitalWrite__(self, channel, value): #self.checkDigitalChannelExported(channel) #self.checkPostingValueAllowed() try: - if (value == 1): - self.valueFile[channel].write('1') + if value == 1: + value = '1' else: - self.valueFile[channel].write('0') - self.valueFile[channel].seek(0) - pass + value = '0' + try: + self.valueFile[channel].write('1') + self.valueFile[channel].seek(0) + except: + command = 'sudo python3 -m myDevices.devices.writevalue -f {} -v {}'.format(self.__getValueFilePath__(channel), value) + subprocess.call(command.split()) except: pass @@ -264,13 +284,18 @@ def __setFunction__(self, channel, value): self.checkDigitalChannelExported(channel) self.checkPostingFunctionAllowed() try: - if (value == self.IN): - self.functionFile[channel].write("in") + if value == self.IN: + value = 'in' else: - self.functionFile[channel].write("out") - self.functionFile[channel].seek(0) + value = 'out' + try: + self.functionFile[channel].write(value) + self.functionFile[channel].seek(0) + except: + command = 'sudo python3 -m myDevices.devices.writevalue -f {} -v {}'.format(self.__getFunctionFilePath__(channel), value) + subprocess.call(command.split()) except Exception as ex: - error('Failed on __setFunction__: ' + str(channel) + ' ' + str(ex)) + exception('Failed on __setFunction__: ' + str(channel) + ' ' + str(ex)) pass def __portRead__(self): diff --git a/myDevices/devices/writevalue.py b/myDevices/devices/writevalue.py index 8becd63..31f8a6e 100644 --- a/myDevices/devices/writevalue.py +++ b/myDevices/devices/writevalue.py @@ -5,10 +5,29 @@ # Write value to file in script so it can be called via sudo setInfo() logToFile() + write_bytearray = False + filepath = None + mode = 'w' + value = None + i = 1 + while i < len(sys.argv): + if sys.argv[i] in ["-f", "-F", "--file"]: + filepath = sys.argv[i + 1] + i += 1 + elif sys.argv[i] in ["-v", "-V", "--value"]: + value = sys.argv[i + 1] + i += 1 + elif sys.argv[i] in ["-b", "-B", "--bytearray"]: + write_bytearray = True + mode = 'wb' + i += 1 try: - info('Write value {} to {}'.format(sys.argv[2], sys.argv[1])) - with open(sys.argv[1], "wb") as out_file: - out_file.write(bytearray([int(sys.argv[2])])) + info('Write value {} to {}'.format(value, filepath)) + with open(filepath, mode) as out_file: + if write_bytearray: + out_file.write(bytearray([int(value)])) + else: + out_file.write(value) except Exception as ex: error('Error writing value {}'.format(ex)) From be03842d7ddf2572a9f41d4822134955a8811e8f Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 28 Jun 2017 17:17:41 -0600 Subject: [PATCH 020/129] Read/write GPIO data using sudo from a non-root user process. --- myDevices/devices/digital/ds2408.py | 2 +- myDevices/devices/digital/gpio.py | 64 ++++++++++++++++------------- myDevices/devices/readvalue.py | 21 ++++++++++ myDevices/devices/writevalue.py | 11 ++--- myDevices/sensors/sensors.py | 8 +--- 5 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 myDevices/devices/readvalue.py diff --git a/myDevices/devices/digital/ds2408.py b/myDevices/devices/digital/ds2408.py index 37a1a83..0a53b72 100644 --- a/myDevices/devices/digital/ds2408.py +++ b/myDevices/devices/digital/ds2408.py @@ -78,7 +78,7 @@ def readByte(self): def writeByte(self, value): try: info('DS2408 writeByte {} {} {}'.format(self.slave, value, bytearray([value]))) - command = 'sudo python3 -m myDevices.devices.writevalue -b -f /sys/bus/w1/devices/{}/output -v {}'.format(self.slave, value) + command = 'sudo python3 -m myDevices.devices.writevalue -f /sys/bus/w1/devices/{}/output -b {}'.format(self.slave, value) subprocess.call(command.split()) except Exception as ex: error('DS2408 writeByte error: {}'.format(ex)) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 6db1f4b..89cc7e5 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -21,28 +21,21 @@ from myDevices.devices.digital import GPIOPort from myDevices.decorators.rest import request, response from myDevices.system.hardware import MAPPING +from myDevices.system.services import ServiceManager try: import ASUS.GPIO as gpio_library except: gpio_library = None + FSEL_OFFSET = 0 # 0x0000 PINLEVEL_OFFSET = 13 # 0x0034 / 4 BLOCK_SIZE = (4*1024) -FUNCTIONS = ["IN", "OUT", "ALT5", "ALT4", "ALT0", "ALT1", "ALT2", "ALT3", "PWM"] - class NativeGPIO(GPIOPort): IN = 0 OUT = 1 - ALT5 = 2 - ALT4 = 3 - ALT0 = 4 - ALT1 = 5 - ALT2 = 6 - ALT3 = 7 - PWM = 8 ASUS_GPIO = 44 @@ -192,15 +185,19 @@ def __checkFilesystemFunction__(self, channel): valRet = self.__checkFilesystemExport__(channel) if not valRet: return + mode = 'w+' + if gpio_library and os.geteuid() != 0: + #On ASUS device open the file in read mode from non-root process + mode = 'r' for i in range(10): try: - self.functionFile[channel] = open(self.__getFunctionFilePath__(channel), "w+") + self.functionFile[channel] = open(self.__getFunctionFilePath__(channel), mode) break except PermissionError: # Try again since the file group might not have been set to the gpio group # since there is a delay when the gpio channel is first exported sleep(0.01) - + def __checkFilesystemValue__(self, channel): if not self.valueFile[channel]: @@ -208,9 +205,13 @@ def __checkFilesystemValue__(self, channel): valRet = self.__checkFilesystemExport__(channel) if not valRet: return + mode = 'w+' + if gpio_library and os.geteuid() != 0: + #On ASUS device open the file in read mode from non-root process + mode = 'r' for i in range(10): try: - self.valueFile[channel] = open(self.__getValueFilePath__(channel), "w+") + self.valueFile[channel] = open(self.__getValueFilePath__(channel), mode) break except PermissionError: # Try again since the file group might not have been set to the gpio group @@ -221,14 +222,13 @@ def __digitalRead__(self, channel): self.__checkFilesystemValue__(channel) #self.checkDigitalChannelExported(channel) try: - r = self.valueFile[channel].read(1) + value = self.valueFile[channel].read(1) self.valueFile[channel].seek(0) - if r[0] == '1': + if value[0] == '1': return self.HIGH else: return self.LOW except: - #error('Error' + str(ex)) return -1 def __digitalWrite__(self, channel, value): @@ -244,7 +244,7 @@ def __digitalWrite__(self, channel, value): self.valueFile[channel].write('1') self.valueFile[channel].seek(0) except: - command = 'sudo python3 -m myDevices.devices.writevalue -f {} -v {}'.format(self.__getValueFilePath__(channel), value) + command = 'sudo python3 -m myDevices.devices.writevalue -f {} -t {}'.format(self.__getValueFilePath__(channel), value) subprocess.call(command.split()) except: pass @@ -264,19 +264,23 @@ def __getFunction__(self, channel): self.checkDigitalChannelExported(channel) try: if gpio_library: - value = gpio_library.gpio_function(channel) + if os.geteuid() == 0: + value = gpio_library.gpio_function(channel) + else: + value, error = ServiceManager.ExecuteCommand('sudo python3 -m myDevices.devices.readvalue -c {}'.format(channel)) + return int(value.splitlines()[0]) # If this is not a GPIO function return it, otherwise check the function file to see # if it is an IN or OUT pin since the ASUS library doesn't provide that info. if value != self.ASUS_GPIO: return value - r = self.functionFile[channel].read() + function = self.functionFile[channel].read() self.functionFile[channel].seek(0) - if r.startswith("out"): + if function.startswith("out"): return self.OUT else: return self.IN except Exception as ex: - # error('Failed on __getFunction__: '+ str(channel) + ' ' + str(ex)) + error('Failed on __getFunction__: '+ str(channel) + ' ' + str(ex)) return -1 def __setFunction__(self, channel, value): @@ -292,7 +296,7 @@ def __setFunction__(self, channel, value): self.functionFile[channel].write(value) self.functionFile[channel].seek(0) except: - command = 'sudo python3 -m myDevices.devices.writevalue -f {} -v {}'.format(self.__getFunctionFilePath__(channel), value) + command = 'sudo python3 -m myDevices.devices.writevalue -f {} -t {}'.format(self.__getFunctionFilePath__(channel), value) subprocess.call(command.split()) except Exception as ex: exception('Failed on __setFunction__: ' + str(channel) + ' ' + str(ex)) @@ -315,13 +319,18 @@ def __portWrite__(self, value): #@request("GET", "*") @response(contentType=M_JSON) def wildcard(self, compact=False): + if gpio_library and os.geteuid() != 0: + #If not root on an ASUS device get the pin states as root + value, error = ServiceManager.ExecuteCommand('sudo python3 -m myDevices.devices.readvalue --pins') + value = value.splitlines()[0] + import json + return json.loads(value) if compact: f = "f" v = "v" else: f = "function" v = "value" - values = {} for i in self.pins: if compact: @@ -337,14 +346,11 @@ def getFunction(self, channel): def getFunctionString(self, channel): f = self.getFunction(channel) function_string = 'UNKNOWN' + functions = {0:'IN', 1:'OUT', 2:'ALT5', 3:'ATL4', 4:'ALT0', 5:'ALT1', 6:'ALT2', 7:'ALT3', 8:'PWM', + 40:'SERIAL', 41:'SPI', 42:'I2C', 43:'PWM', 44:'GPIO', 45:'TS_XXXX', 46:'RESERVED', 47:'I2S'} if f >= 0: try: - function_string = FUNCTIONS[f] + function_string = functions[f] except: pass - return function_string - - def input(self, channel): - value = self.__digitalRead__(channel) - return value - + return function_string diff --git a/myDevices/devices/readvalue.py b/myDevices/devices/readvalue.py new file mode 100644 index 0000000..aeaf155 --- /dev/null +++ b/myDevices/devices/readvalue.py @@ -0,0 +1,21 @@ +import sys +from myDevices.utils.logger import setInfo, info, error, logToFile +from myDevices.devices.digital.gpio import NativeGPIO + +if __name__ == '__main__': + # Read data using script so it can be called via sudo + setInfo() + logToFile() + i = 1 + while i < len(sys.argv): + if sys.argv[i] in ['-p', '--pins']: + import json + gpio = NativeGPIO() + print(json.dumps(gpio.wildcard())) + if sys.argv[i] in ['-c', '--channel']: + import json + gpio = NativeGPIO() + i += 1 + channel = int(sys.argv[i]) + print(gpio.getFunction(channel)) + i += 1 diff --git a/myDevices/devices/writevalue.py b/myDevices/devices/writevalue.py index 31f8a6e..3ba6af2 100644 --- a/myDevices/devices/writevalue.py +++ b/myDevices/devices/writevalue.py @@ -5,7 +5,6 @@ # Write value to file in script so it can be called via sudo setInfo() logToFile() - write_bytearray = False filepath = None mode = 'w' value = None @@ -14,20 +13,18 @@ if sys.argv[i] in ["-f", "-F", "--file"]: filepath = sys.argv[i + 1] i += 1 - elif sys.argv[i] in ["-v", "-V", "--value"]: + elif sys.argv[i] in ["-t", "-T", "--text"]: value = sys.argv[i + 1] i += 1 elif sys.argv[i] in ["-b", "-B", "--bytearray"]: - write_bytearray = True + value = bytearray([int(sys.argv[i + 1])]) mode = 'wb' + i += 1 i += 1 try: info('Write value {} to {}'.format(value, filepath)) with open(filepath, mode) as out_file: - if write_bytearray: - out_file.write(bytearray([int(value)])) - else: - out_file.write(value) + out_file.write(value) except Exception as ex: error('Error writing value {}'.format(ex)) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index fb143a1..76b565f 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -270,13 +270,7 @@ def BusInfo(self): json = {} for (bus, value) in BUSLIST.items(): json[bus] = int(value["enabled"]) - pin_states = {} - pins = [pin for pin in MAPPING if type(pin) is int] - for pin in pins: - pin_states[pin] = {} - pin_states[pin]['function'] = self.gpio.getFunctionString(pin) - pin_states[pin]['value'] = int(self.gpio.input(pin)) - json['GPIO'] = pin_states + json['GPIO'] = self.gpio.wildcard() json['GpioMap'] = MAPPING self.currentBusInfo = json return self.currentBusInfo From 9b29420211cd1a3c6f04a9b0c84ed5bca4313781 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 29 Jun 2017 17:39:23 -0600 Subject: [PATCH 021/129] Provide on-board GPIO mapping via the NativeGPIO class. --- README.rst | 2 +- myDevices/devices/digital/__init__.py | 2 -- myDevices/devices/digital/gpio.py | 26 ++++++++++++++------ myDevices/devices/readvalue.py | 9 ++++--- myDevices/devices/writevalue.py | 11 ++++----- myDevices/sensors/sensors.py | 3 +-- myDevices/system/hardware.py | 8 +------ myDevices/test/gpio_test.py | 31 ++++++++++++++++++++++++ myDevices/test/hardware_test.py | 6 +---- myDevices/utils/subprocess.py | 34 +++++++++++++++++++++++++++ 10 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 myDevices/test/gpio_test.py create mode 100644 myDevices/utils/subprocess.py diff --git a/README.rst b/README.rst index edc0417..8ac980b 100644 --- a/README.rst +++ b/README.rst @@ -170,7 +170,7 @@ Hardware Info Hardware info, including make, model, etc. is retrieved via ``myDevices.system.hardware.py``. This should be modified or overridden to provide the appropriate hardware info for your board. Pin Mapping - The mapping of the on-board pins is provided in ``myDevices.system.hardware.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab. + The mapping of the on-board pins is provided in ``myDevices.devices.digital.gpio.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab. Settings -------- diff --git a/myDevices/devices/digital/__init__.py b/myDevices/devices/digital/__init__.py index 96568db..fdd8a63 100644 --- a/myDevices/devices/digital/__init__.py +++ b/myDevices/devices/digital/__init__.py @@ -80,7 +80,6 @@ def setFunction(self, channel, value): self.__setFunction__(channel, value) return self.__getFunction__(channel) - #@request("POST", "%(channel)d/function/%(value)s") def setFunctionString(self, channel, value): value = value.lower() @@ -94,7 +93,6 @@ def setFunctionString(self, channel, value): raise ValueError("Bad Function") return self.getFunctionString(channel) - #@request("GET", "%(channel)d/value") @response("%d") def digitalRead(self, channel): diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 89cc7e5..e918387 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -20,8 +20,8 @@ from myDevices.utils.logger import debug, info, error, exception from myDevices.devices.digital import GPIOPort from myDevices.decorators.rest import request, response -from myDevices.system.hardware import MAPPING -from myDevices.system.services import ServiceManager +from myDevices.system.hardware import BOARD_REVISION, CPU_REVISION +from myDevices.utils.subprocess import executeCommand try: import ASUS.GPIO as gpio_library except: @@ -49,13 +49,14 @@ class NativeGPIO(GPIOPort): RATIO = 1 ANGLE = 2 - DIRECTION_FILE = "/sys/class/gpio/gpio%s/direction" + MAPPING = [] instance = None def __init__(self): if not NativeGPIO.instance: - self.pins = [pin for pin in MAPPING if type(pin) is int] + self.setPinMapping() + self.pins = [pin for pin in self.MAPPING if type(pin) is int] GPIOPort.__init__(self, max(self.pins) + 1) self.post_value = True self.post_function = True @@ -267,7 +268,7 @@ def __getFunction__(self, channel): if os.geteuid() == 0: value = gpio_library.gpio_function(channel) else: - value, error = ServiceManager.ExecuteCommand('sudo python3 -m myDevices.devices.readvalue -c {}'.format(channel)) + value, error = executeCommand('sudo python3 -m myDevices.devices.readvalue -c {}'.format(channel)) return int(value.splitlines()[0]) # If this is not a GPIO function return it, otherwise check the function file to see # if it is an IN or OUT pin since the ASUS library doesn't provide that info. @@ -321,7 +322,7 @@ def __portWrite__(self, value): def wildcard(self, compact=False): if gpio_library and os.geteuid() != 0: #If not root on an ASUS device get the pin states as root - value, error = ServiceManager.ExecuteCommand('sudo python3 -m myDevices.devices.readvalue --pins') + value, error = executeCommand('sudo python3 -m myDevices.devices.readvalue --pins') value = value.splitlines()[0] import json return json.loads(value) @@ -353,4 +354,15 @@ def getFunctionString(self, channel): function_string = functions[f] except: pass - return function_string + return function_string + + def setPinMapping(self): + if CPU_REVISION == "0000": + self.MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] + else: + if BOARD_REVISION == 1: + self.MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + elif BOARD_REVISION == 2: + self.MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + elif BOARD_REVISION == 3: + self.MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] diff --git a/myDevices/devices/readvalue.py b/myDevices/devices/readvalue.py index aeaf155..73c60e2 100644 --- a/myDevices/devices/readvalue.py +++ b/myDevices/devices/readvalue.py @@ -1,11 +1,11 @@ import sys -from myDevices.utils.logger import setInfo, info, error, logToFile +# from myDevices.utils.logger import setInfo, info, error, logToFile from myDevices.devices.digital.gpio import NativeGPIO if __name__ == '__main__': - # Read data using script so it can be called via sudo - setInfo() - logToFile() + # Read data using script so it can be called via sudo, sends the data to the main process by writing to stdout + # setInfo() + # logToFile() i = 1 while i < len(sys.argv): if sys.argv[i] in ['-p', '--pins']: @@ -13,7 +13,6 @@ gpio = NativeGPIO() print(json.dumps(gpio.wildcard())) if sys.argv[i] in ['-c', '--channel']: - import json gpio = NativeGPIO() i += 1 channel = int(sys.argv[i]) diff --git a/myDevices/devices/writevalue.py b/myDevices/devices/writevalue.py index 3ba6af2..62ba152 100644 --- a/myDevices/devices/writevalue.py +++ b/myDevices/devices/writevalue.py @@ -1,10 +1,10 @@ import sys -from myDevices.utils.logger import setInfo, info, error, logToFile +# from myDevices.utils.logger import setInfo, info, error, logToFile if __name__ == '__main__': # Write value to file in script so it can be called via sudo - setInfo() - logToFile() + # setInfo() + # logToFile() filepath = None mode = 'w' value = None @@ -22,9 +22,8 @@ i += 1 i += 1 try: - info('Write value {} to {}'.format(value, filepath)) + # info('Write value {} to {}'.format(value, filepath)) with open(filepath, mode) as out_file: out_file.write(value) except Exception as ex: - error('Error writing value {}'.format(ex)) - + print('Error writing value {}'.format(ex), file=sys.stderr) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 76b565f..3c8460b 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -15,7 +15,6 @@ from myDevices.utils.threadpool import ThreadPool from hashlib import sha1 import urllib.request as req -from myDevices.system.hardware import MAPPING from myDevices.devices.bus import checkAllBus, BUSLIST from myDevices.devices.digital.gpio import NativeGPIO as GPIO from myDevices.devices import manager @@ -271,7 +270,7 @@ def BusInfo(self): for (bus, value) in BUSLIST.items(): json[bus] = int(value["enabled"]) json['GPIO'] = self.gpio.wildcard() - json['GpioMap'] = MAPPING + json['GpioMap'] = self.gpio.MAPPING self.currentBusInfo = json return self.currentBusInfo diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index 8f2a747..be0f3be 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -9,7 +9,6 @@ BOARD_REVISION = 0 CPU_REVISION = "0" -MAPPING = [] try: with open("/proc/cpuinfo") as f: @@ -20,19 +19,14 @@ CPU_REVISION = result.group(1) if CPU_REVISION.startswith("1000"): CPU_REVISION = CPU_REVISION[-4:] - if CPU_REVISION == "0000": - MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] - else: + if CPU_REVISION != "0000": cpurev = int(CPU_REVISION, 16) if cpurev < 0x04: BOARD_REVISION = 1 - MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] elif cpurev < 0x10: BOARD_REVISION = 2 - MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] else: BOARD_REVISION = 3 - MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] except: exception("Error reading cpuinfo") diff --git a/myDevices/test/gpio_test.py b/myDevices/test/gpio_test.py new file mode 100644 index 0000000..aa60eb0 --- /dev/null +++ b/myDevices/test/gpio_test.py @@ -0,0 +1,31 @@ +import time +import unittest +from myDevices.utils.logger import exception, setDebug, info, debug, error, logToFile, setInfo +from myDevices.devices.digital.gpio import NativeGPIO + +class GpioTest(unittest.TestCase): + def testGPIO(self): + gpio = NativeGPIO() + pins = [pin for pin in gpio.MAPPING if type(pin) is int] + for pin in pins: + info('Testing pin {}'.format(pin)) + start = time.time() + function = gpio.setFunctionString(pin, "OUT") + info('testGPIO setFunctionString, elapsed time {}'.format(time.time() - start)) + if function == "UNKNOWN": + info('Pin {} function UNKNOWN, skipping'.format(pin)) + continue + self.assertEqual("OUT", function) + start = time.time() + value = gpio.digitalWrite(pin, 1) + info('testGPIO digitalWrite, elapsed time {}'.format(time.time() - start)) + self.assertEqual(value, 1) + start = time.time() + value = gpio.digitalWrite(pin, 0) + info('testGPIO digitalWrite, elapsed time {}'.format(time.time() - start)) + self.assertEqual(value, 0) + + +if __name__ == '__main__': + setInfo() + unittest.main() diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py index ff8db9c..e31412f 100644 --- a/myDevices/test/hardware_test.py +++ b/myDevices/test/hardware_test.py @@ -1,6 +1,6 @@ import unittest from myDevices.utils.logger import setInfo, info -from myDevices.system.hardware import Hardware, MAPPING, BOARD_REVISION, CPU_REVISION +from myDevices.system.hardware import Hardware, BOARD_REVISION, CPU_REVISION class HarwareTest(unittest.TestCase): def setUp(self): @@ -22,10 +22,6 @@ def testGetMac(self): info(mac) self.assertRegex(mac, '^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$') - def testMapping(self): - info(MAPPING) - self.assertTrue(MAPPING) - def testBoardRevision(self): info(BOARD_REVISION) self.assertGreaterEqual(BOARD_REVISION, 0) diff --git a/myDevices/utils/subprocess.py b/myDevices/utils/subprocess.py new file mode 100644 index 0000000..8026a02 --- /dev/null +++ b/myDevices/utils/subprocess.py @@ -0,0 +1,34 @@ +from subprocess import Popen, PIPE +from myDevices.utils.logger import debug, info, error, exception + +def setMemoryLimits(): + """Set memory limit when launching a process to the default maximum""" + try: + from resource import getrlimit, setrlimit, RLIMIT_AS + soft, hard = getrlimit(RLIMIT_AS) + setrlimit(RLIMIT_AS, (hard, hard)) + except: + pass + +def executeCommand(command, increaseMemoryLimit=False): + """Execute a specified command, increasing the processes memory limits if specified""" + debug('executeCommand: ' + command) + output = '' + returncode = 1 + try: + setLimit = None + if increaseMemoryLimit: + setLimit = setMemoryLimits + process = Popen(command, stdout=PIPE, shell=True, preexec_fn=setLimit) + processOutput = process.communicate() + returncode = process.wait() + returncode = process.returncode + debug('executeCommand: ' + str(processOutput)) + if processOutput and processOutput[0]: + output = str(processOutput[0].decode('utf-8')) + processOutput = None + except: + exception('executeCommand failed: ' + command) + debug('executeCommand: ' + command + ' ' + str(output)) + retOut = str(output) + return (retOut, returncode) From 3fd3c7c9c67338d378f49c92012a738d6cc3fccc Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 30 Jun 2017 11:57:30 -0600 Subject: [PATCH 022/129] After setting GPIO function on Tinker Board just read function file to get state rather than launching a root process. --- myDevices/devices/digital/gpio.py | 12 +++++++++--- myDevices/test/gpio_test.py | 7 ------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index e918387..e4068a5 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -20,7 +20,7 @@ from myDevices.utils.logger import debug, info, error, exception from myDevices.devices.digital import GPIOPort from myDevices.decorators.rest import request, response -from myDevices.system.hardware import BOARD_REVISION, CPU_REVISION +from myDevices.system.hardware import BOARD_REVISION, Hardware from myDevices.utils.subprocess import executeCommand try: import ASUS.GPIO as gpio_library @@ -63,6 +63,7 @@ def __init__(self): self.gpio_setup = [] self.gpio_reset = [] self.gpio_map = None + self.pinFunctionSet = set() self.valueFile = {pin:None for pin in self.pins} self.functionFile = {pin:None for pin in self.pins} for pin in self.pins: @@ -264,7 +265,11 @@ def __getFunction__(self, channel): self.__checkFilesystemFunction__(channel) self.checkDigitalChannelExported(channel) try: - if gpio_library: + # If we haven't already set the channel function on an ASUS device, we use the GPIO + # library to get the function. Otherwise we just fall through and read the file itself + # since we can assume the pin is a GPIO pin and reading the function file is quicker + # than launching a separate process. + if gpio_library and channel not in self.pinFunctionSet: if os.geteuid() == 0: value = gpio_library.gpio_function(channel) else: @@ -299,6 +304,7 @@ def __setFunction__(self, channel, value): except: command = 'sudo python3 -m myDevices.devices.writevalue -f {} -t {}'.format(self.__getFunctionFilePath__(channel), value) subprocess.call(command.split()) + self.pinFunctionSet.add(channel) except Exception as ex: exception('Failed on __setFunction__: ' + str(channel) + ' ' + str(ex)) pass @@ -357,7 +363,7 @@ def getFunctionString(self, channel): return function_string def setPinMapping(self): - if CPU_REVISION == "0000": + if Hardware().getModel() == 'Tinker Board': self.MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] else: if BOARD_REVISION == 1: diff --git a/myDevices/test/gpio_test.py b/myDevices/test/gpio_test.py index aa60eb0..e7c0991 100644 --- a/myDevices/test/gpio_test.py +++ b/myDevices/test/gpio_test.py @@ -1,4 +1,3 @@ -import time import unittest from myDevices.utils.logger import exception, setDebug, info, debug, error, logToFile, setInfo from myDevices.devices.digital.gpio import NativeGPIO @@ -9,20 +8,14 @@ def testGPIO(self): pins = [pin for pin in gpio.MAPPING if type(pin) is int] for pin in pins: info('Testing pin {}'.format(pin)) - start = time.time() function = gpio.setFunctionString(pin, "OUT") - info('testGPIO setFunctionString, elapsed time {}'.format(time.time() - start)) if function == "UNKNOWN": info('Pin {} function UNKNOWN, skipping'.format(pin)) continue self.assertEqual("OUT", function) - start = time.time() value = gpio.digitalWrite(pin, 1) - info('testGPIO digitalWrite, elapsed time {}'.format(time.time() - start)) self.assertEqual(value, 1) - start = time.time() value = gpio.digitalWrite(pin, 0) - info('testGPIO digitalWrite, elapsed time {}'.format(time.time() - start)) self.assertEqual(value, 0) From 2104f1e81b98b49d7d10cf592bf622f53c95bf89 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 30 Jun 2017 18:04:35 -0600 Subject: [PATCH 023/129] Use subprocess module for executing commands. --- myDevices/cloud/client.py | 7 +++-- myDevices/cloud/updater.py | 8 +++--- myDevices/devices/readvalue.py | 5 ++++ myDevices/devices/writevalue.py | 5 ++++ myDevices/system/raspiconfig.py | 8 +++--- myDevices/system/services.py | 49 ++++----------------------------- myDevices/test/sensors_test.py | 31 ++++++++++++--------- myDevices/utils/daemon.py | 4 +-- myDevices/utils/subprocess.py | 3 ++ myDevices/wifi/WifiManager.py | 4 +-- myDevices/wifi/WirelessLib.py | 5 ++-- 11 files changed, 55 insertions(+), 74 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ddfeee5..818a3f0 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -25,6 +25,7 @@ from myDevices.utils.daemon import Daemon from myDevices.utils.threadpool import ThreadPool from myDevices.utils.history import History +from myDevices.utils.subprocess import executeCommand from select import select from hashlib import sha256 from myDevices.cloud.apiclient import CayenneApiClient @@ -685,7 +686,7 @@ def ExecuteMessage(self, messageObject): return if packetType == PacketTypes.PT_UNINSTALL_AGENT.value: command = "sudo /etc/myDevices/uninstall/uninstall.sh" - services.ServiceManager.ExecuteCommand(command) + executeCommand(command) return if packetType == PacketTypes.PT_STARTUP_APPLICATIONS.value: self.BuildPT_STARTUP_APPLICATIONS() @@ -711,7 +712,7 @@ def ExecuteMessage(self, messageObject): data['Message'] = 'Computer Restarted!' self.EnqueuePacket(data) command = "sudo shutdown -r now" - services.ServiceManager.ExecuteCommand(command) + executeCommand(command) return if packetType == PacketTypes.PT_SHUTDOWN_COMPUTER.value: info(PacketTypes.PT_SHUTDOWN_COMPUTER) @@ -721,7 +722,7 @@ def ExecuteMessage(self, messageObject): data['Message'] = 'Computer Powered Off!' self.EnqueuePacket(data) command = "sudo shutdown -h now" - services.ServiceManager.ExecuteCommand(command) + executeCommand(command) return if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: info('PT_AGENT_CONFIGURATION: ' + str(messageObject.Data)) diff --git a/myDevices/cloud/updater.py b/myDevices/cloud/updater.py index 81a6d14..742dd8f 100644 --- a/myDevices/cloud/updater.py +++ b/myDevices/cloud/updater.py @@ -1,7 +1,6 @@ from myDevices.utils.logger import exception, info, warn, error, debug, setDebug from time import time, sleep from sched import scheduler -from myDevices.system import services from distutils.version import LooseVersion, StrictVersion from os import mkdir, path from threading import Thread @@ -9,6 +8,7 @@ from datetime import datetime, timedelta import random from myDevices.utils.config import Config +from myDevices.utils.subprocess import executeCommand SETUP_NAME = 'myDevicesSetup_raspberrypi.sh' INSTALL_PATH = '/etc/myDevices/' @@ -105,7 +105,7 @@ def CheckUpdate(self): return sleep(1) # Run the update as root - retCode = services.ServiceManager.ExecuteCommand("sudo python3 -m myDevices.cloud.doupdatecheck") + executeCommand("sudo python3 -m myDevices.cloud.doupdatecheck") def DoUpdateCheck(self): mkdir(UPDATE_PATH) @@ -184,11 +184,11 @@ def ExecuteUpdate(self): if retValue is False: return retValue command = "chmod +x " + SETUP_PATH - (output, returncode) = services.ServiceManager.ExecuteCommand(command) + (output, returncode) = executeCommand(command) del output command = "nohup " + SETUP_PATH + ' -update >/var/log/myDevices/myDevices.update 2>/var/log/myDevices/myDevices.update.err' debug('execute command started: {}'.format(command)) - (output, returncode) = services.ServiceManager.ExecuteCommand(command) + (output, returncode) = executeCommand(command) del output debug('Updater execute command finished') diff --git a/myDevices/devices/readvalue.py b/myDevices/devices/readvalue.py index 73c60e2..9634640 100644 --- a/myDevices/devices/readvalue.py +++ b/myDevices/devices/readvalue.py @@ -1,3 +1,8 @@ +""" +This script reads GPIO info. It is intended to be launched via sudo in order to +read data from files that require root access so the main agent code can run from +a non-root process and only elevate to root when necessary. +""" import sys # from myDevices.utils.logger import setInfo, info, error, logToFile from myDevices.devices.digital.gpio import NativeGPIO diff --git a/myDevices/devices/writevalue.py b/myDevices/devices/writevalue.py index 62ba152..783cc25 100644 --- a/myDevices/devices/writevalue.py +++ b/myDevices/devices/writevalue.py @@ -1,3 +1,8 @@ +""" +This script writes a value to a file. It is intended to be launched via sudo +in order to write to files that require root access so the main agent code can +run from a non-root process and only elevate to root when necessary. +""" import sys # from myDevices.utils.logger import setInfo, info, error, logToFile diff --git a/myDevices/system/raspiconfig.py b/myDevices/system/raspiconfig.py index 2495c35..3b28351 100644 --- a/myDevices/system/raspiconfig.py +++ b/myDevices/system/raspiconfig.py @@ -2,7 +2,7 @@ This module provides a class for modifying Raspberry Pi configuration settings. """ from myDevices.utils.logger import exception, info, warn, error, debug -from myDevices.system.services import ServiceManager +from myDevices.utils.subprocess import executeCommand from time import sleep from myDevices.utils.threadpool import ThreadPool @@ -16,7 +16,7 @@ def ExpandRootfs(): """Expand the filesystem""" command = "sudo raspi-config --expand-rootfs" debug('ExpandRootfs command:' + command) - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = executeCommand(command) debug('ExpandRootfs command:' + command + " retCode: " + returnCode) output = 'reboot required' return (returnCode, output) @@ -33,7 +33,7 @@ def ExecuteConfigCommand(config_id, parameters): if config_id == 0: return RaspiConfig.ExpandRootfs() command = "sudo " + CUSTOM_CONFIG_SCRIPT + " " + str(config_id) + " " + str(parameters) - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = executeCommand(command) debug('ExecuteConfigCommand '+ str(config_id) + ' args: ' + str(parameters) + ' retCode: ' + str(returnCode) + ' output: ' + output ) if "reboot required" in output: ThreadPool.Submit(RaspiConfig.RestartService) @@ -44,7 +44,7 @@ def RestartService(): """Reboot the device""" sleep(5) command = "sudo shutdown -r now" - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = executeCommand(command) @staticmethod def getConfig(): diff --git a/myDevices/system/services.py b/myDevices/system/services.py index 0c08430..0d7ed29 100644 --- a/myDevices/system/services.py +++ b/myDevices/system/services.py @@ -6,6 +6,8 @@ from threading import RLock from psutil import Process, process_iter, virtual_memory, cpu_percent from myDevices.utils.logger import exception, info, warn, error, debug +from myDevices.utils.subprocess import executeCommand + class ProcessInfo: """Class for getting process info and killing processes""" @@ -161,7 +163,7 @@ def Run(self): """Get info about services""" debug('ServiceManager::Run') with self.mutex: - (output, returnCode) = ServiceManager.ExecuteCommand("service --status-all") + (output, returnCode) = executeCommand("service --status-all") servicesList = output.split("\n") service_names = [] for line in servicesList: @@ -199,7 +201,7 @@ def Start(self, serviceName): """Start the named service""" debug('ServiceManager::Start') command = "sudo service " + serviceName + " start" - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = executeCommand(command) debug('ServiceManager::Start command:' + command + " output: " + output) del output return returnCode @@ -208,7 +210,7 @@ def Stop(self, serviceName): """Stop the named service""" debug('ServiceManager::Stop') command = "sudo service " + serviceName + " stop" - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = executeCommand(command) debug('ServiceManager::Stop command:' + command + " output: " + output) del output return returnCode @@ -217,46 +219,7 @@ def Status(self, serviceName): """Get the status of the named service""" debug('ServiceManager::Status') command = "service " + serviceName + " status" - (output, returnCode) = ServiceManager.ExecuteCommand(command) + (output, returnCode) = executeCommand(command) debug('ServiceManager::Stop command:' + command + " output: " + output) del output return returnCode - - @staticmethod - def SetMemoryLimits(): - """Set memory limit when launching a process to the default maximum""" - try: - from resource import getrlimit, setrlimit, RLIMIT_AS - soft, hard = getrlimit(RLIMIT_AS) - setrlimit(RLIMIT_AS, (hard, hard)) - except: - pass - - @staticmethod - def ExecuteCommand(command, increaseMemoryLimit=False): - """Execute a specified command, increasing the processes memory limits if specified""" - debug('ServiceManager::ExecuteCommand: ' + command) - output = "" - returncode = 1 - try: - setLimit = None - if increaseMemoryLimit: - setLimit = ServiceManager.SetMemoryLimits - process = Popen(command, stdout=PIPE, shell=True, preexec_fn=setLimit) - processOutput = process.communicate() - returncode = process.wait() - returncode = process.returncode - debug('ServiceManager::ExecuteCommand: ' + str(processOutput)) - if processOutput and processOutput[0]: - output = str(processOutput[0].decode('utf-8')) - processOutput = None - except OSError as oserror: - warn('ServiceManager::ExecuteCommand handled: ' + command + ' Exception:' + str(traceback.format_exc())) - from myDevices.utils.daemon import Daemon - Daemon.OnFailure('services', oserror.errno) - except: - exception('ServiceManager::ExecuteCommand failed: ' + command) - debug('ServiceManager::ExecuteCommand: ' + command + ' ' + str(output)) - retOut = str(output) - del output - return (retOut, returncode) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index c7aaf78..53317b9 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -12,6 +12,7 @@ from time import sleep from json import loads, dumps + class SensorsClientTest(unittest.TestCase): @classmethod def setUpClass(cls): @@ -37,16 +38,18 @@ def testBusInfo(self): self.assertEqual(set(bus.keys()), set(['GpioMap', 'SPI', 'GPIO', 'ONEWIRE', 'I2C', 'UART'])) def testSetFunction(self): - self.setChannelFunction(5, 'IN') - self.setChannelFunction(5, 'OUT') + self.setChannelFunction(GPIO.instance.pins[0], 'IN') + self.setChannelFunction(GPIO.instance.pins[0], 'OUT') def testSetValue(self): - self.setChannelValue(5, 1) - self.setChannelValue(5, 0) + self.setChannelFunction(GPIO.instance.pins[0], 'OUT') + self.setChannelValue(GPIO.instance.pins[0], 1) + self.setChannelValue(GPIO.instance.pins[0], 0) def testSensors(self): #Test adding a sensor - testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': 12}, 'name': 'testdevice'} + channel = GPIO.instance.pins[4] + testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} compareKeys = ('args', 'description', 'device') retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) self.assertTrue(retValue) @@ -55,7 +58,7 @@ def testSensors(self): self.assertEqual(testSensor[key], retrievedSensor[key]) #Test updating a sensor editedSensor = testSensor - editedSensor['args']['channel'] = 13 + editedSensor['args']['channel'] = GPIO.instance.pins[5] retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) self.assertTrue(retValue) retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) @@ -68,9 +71,11 @@ def testSensors(self): self.assertNotIn(testSensor['name'], deviceNames) def testSensorInfo(self): - sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': 16}, 'name': 'test_actuator'}, - 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, - 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'}} + channel = GPIO.instance.pins[6] + sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'test_actuator'}, + # 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, + # 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} + } for sensor in sensors.values(): SensorsClientTest.client.AddSensor(sensor['name'], sensor['description'], sensor['device'], sensor['args']) SensorsClientTest.client.SensorsInfo() @@ -78,8 +83,8 @@ def testSensorInfo(self): self.setSensorValue(sensors['actuator'], 1) self.setSensorValue(sensors['actuator'], 0) #Test getting analog value - retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensors['distance']['name']) - self.assertEqual(retrievedSensorInfo['float'], 0.0) + # retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensors['distance']['name']) + # self.assertEqual(retrievedSensorInfo['float'], 0.0) for sensor in sensors.values(): self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) @@ -95,12 +100,12 @@ def setSensorValue(self, sensor, value): def setChannelFunction(self, channel, function): SensorsClientTest.client.gpio.setFunctionString(channel, function) bus = SensorsClientTest.client.BusInfo() - self.assertEqual(function, bus['GPIO'][channel]['function']) + self.assertEqual(function, bus['GPIO'][str(channel)]['function']) def setChannelValue(self, channel, value): SensorsClientTest.client.gpio.digitalWrite(channel, value) bus = SensorsClientTest.client.BusInfo() - self.assertEqual(value, bus['GPIO'][channel]['value']) + self.assertEqual(value, bus['GPIO'][str(channel)]['value']) if __name__ == '__main__': setInfo() diff --git a/myDevices/utils/daemon.py b/myDevices/utils/daemon.py index ce9ad50..0d7ff9f 100644 --- a/myDevices/utils/daemon.py +++ b/myDevices/utils/daemon.py @@ -4,7 +4,7 @@ from sys import exit from datetime import datetime from myDevices.utils.logger import exception, info, warn, error, debug -from myDevices.system.services import ServiceManager +from myDevices.utils.subprocess import executeCommand #defining reset timeout in seconds RESET_TIMEOUT = 30 @@ -46,7 +46,7 @@ def Restart(): """Restart the agent daemon""" try: info('Daemon restarting myDevices' ) - (output, returncode) = ServiceManager.ExecuteCommand('sudo service myDevices restart') + (output, returncode) = executeCommand('sudo service myDevices restart') debug(str(output) + ' ' + str(returncode)) del output except: diff --git a/myDevices/utils/subprocess.py b/myDevices/utils/subprocess.py index 8026a02..9768bb7 100644 --- a/myDevices/utils/subprocess.py +++ b/myDevices/utils/subprocess.py @@ -1,3 +1,6 @@ +""" +This module contains functions for launching subprocesses and returning output from them. +""" from subprocess import Popen, PIPE from myDevices.utils.logger import debug, info, error, exception diff --git a/myDevices/wifi/WifiManager.py b/myDevices/wifi/WifiManager.py index 5980eef..9d062e6 100644 --- a/myDevices/wifi/WifiManager.py +++ b/myDevices/wifi/WifiManager.py @@ -1,7 +1,7 @@ from myDevices.wifi.WirelessLib import Wireless from json import dumps, loads, JSONEncoder, JSONDecoder from myDevices.utils.logger import exception, info, warn, error, debug -from myDevices.system.services import ServiceManager +from myDevices.utils.subprocess import executeCommand class Network(): def GetNetworkId(): @@ -16,7 +16,7 @@ def GetNetworkId(): ip = key network = val command = 'arp -n ' + ip + ' | grep ' + network + ' | awk \'{print $3}\'' - (output, retCode) = ServiceManager.ExecuteCommand(command) + (output, retCode) = executeCommand(command) if int(retCode) > 0: return None returnValue['Ip'] = ip diff --git a/myDevices/wifi/WirelessLib.py b/myDevices/wifi/WirelessLib.py index c42163f..ed53447 100644 --- a/myDevices/wifi/WirelessLib.py +++ b/myDevices/wifi/WirelessLib.py @@ -1,12 +1,11 @@ from abc import ABCMeta, abstractmethod import subprocess from time import sleep -from myDevices.system.services import ServiceManager +from myDevices.utils.subprocess import executeCommand # send a command to the shell and return the result def cmd(cmd): response = "" - try: #response = subprocess.Popen( # cmd, shell=True, @@ -14,7 +13,7 @@ def cmd(cmd): #process.stdout.close() #process.stdin.close() #process.stderr.close() - response, returncode = ServiceManager.ExecuteCommand(cmd) + response, returncode = executeCommand(cmd) except: reponse = "Error" return response From 71affd11770d4e9fcf12c852211ded6442ad3374 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 30 Jun 2017 18:33:53 -0600 Subject: [PATCH 024/129] Use writevalue script to export GPIO pins if there is a permission error. Use executeCommand for launching subprocesses. --- myDevices/devices/digital/gpio.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index e4068a5..6267046 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -14,7 +14,6 @@ import os import mmap -import subprocess from time import sleep from myDevices.utils.types import M_JSON from myDevices.utils.logger import debug, info, error, exception @@ -176,6 +175,9 @@ def __checkFilesystemExport__(self, channel): try: with open("/sys/class/gpio/export", "a") as f: f.write("%s" % channel) + except PermissionError: + command = 'sudo python3 -m myDevices.devices.writevalue -f /sys/class/gpio/export -t {}'.format(channel) + executeCommand(command) except Exception as ex: error('Failed on __checkFilesystemExport__: ' + str(channel) + ' ' + str(ex)) return False @@ -247,7 +249,7 @@ def __digitalWrite__(self, channel, value): self.valueFile[channel].seek(0) except: command = 'sudo python3 -m myDevices.devices.writevalue -f {} -t {}'.format(self.__getValueFilePath__(channel), value) - subprocess.call(command.split()) + executeCommand(command) except: pass @@ -303,7 +305,7 @@ def __setFunction__(self, channel, value): self.functionFile[channel].seek(0) except: command = 'sudo python3 -m myDevices.devices.writevalue -f {} -t {}'.format(self.__getFunctionFilePath__(channel), value) - subprocess.call(command.split()) + executeCommand(command) self.pinFunctionSet.add(channel) except Exception as ex: exception('Failed on __setFunction__: ' + str(channel) + ' ' + str(ex)) From 86825d7e3f77cbf91cef2f04749b227612c6799b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 5 Jul 2017 15:21:48 -0600 Subject: [PATCH 025/129] Write correct value to GPIO file. --- myDevices/devices/digital/gpio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 6267046..ee4f4ab 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -245,7 +245,7 @@ def __digitalWrite__(self, channel, value): else: value = '0' try: - self.valueFile[channel].write('1') + self.valueFile[channel].write(value) self.valueFile[channel].seek(0) except: command = 'sudo python3 -m myDevices.devices.writevalue -f {} -t {}'.format(self.__getValueFilePath__(channel), value) From a4dc60ba1b6d85943e02054841ecc347f877db19 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 7 Jul 2017 11:33:54 -0600 Subject: [PATCH 026/129] Check CPU hardware when getting manufacturer/model. --- myDevices/system/hardware.py | 11 ++++++++--- myDevices/test/hardware_test.py | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index be0f3be..0a1320a 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -9,11 +9,12 @@ BOARD_REVISION = 0 CPU_REVISION = "0" +CPU_HARDWARE = "" try: with open("/proc/cpuinfo") as f: - rc = re.compile("Revision\s*:\s(.*)\n") info = f.read() + rc = re.compile("Revision\s*:\s(.*)\n") result = rc.search(info) if result: CPU_REVISION = result.group(1) @@ -27,6 +28,9 @@ BOARD_REVISION = 2 else: BOARD_REVISION = 3 + rc = re.compile("Hardware\s*:\s(.*)\n") + result = rc.search(info) + CPU_HARDWARE = result.group(1) except: exception("Error reading cpuinfo") @@ -49,7 +53,8 @@ def __init__(self): self.model["a21041"] = "Pi 2 Model B" self.model["900092"] = "Zero" self.model["a22082"] = self.model["a02082"] = "Pi 3 Model B" - self.model["0000"] = "Tinker Board" + if "Rockchip" in CPU_HARDWARE: + self.model["0000"] = "Tinker Board" def getManufacturer(self): """Return manufacturer name as string""" @@ -61,7 +66,7 @@ def getManufacturer(self): return "Qisda" if self.Revision in ["0006", "0007", "000d"]: return "Egoman" - if self.Revision == "0000": + if self.Revision == "0000" and "Rockchip" in CPU_HARDWARE: return "ASUS" return "Element14/Premier Farnell" diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py index e31412f..7e1cd6c 100644 --- a/myDevices/test/hardware_test.py +++ b/myDevices/test/hardware_test.py @@ -1,6 +1,6 @@ import unittest from myDevices.utils.logger import setInfo, info -from myDevices.system.hardware import Hardware, BOARD_REVISION, CPU_REVISION +from myDevices.system.hardware import Hardware, BOARD_REVISION, CPU_REVISION, CPU_HARDWARE class HarwareTest(unittest.TestCase): def setUp(self): @@ -31,5 +31,10 @@ def testCpuRevision(self): info(CPU_REVISION) self.assertNotEqual(CPU_REVISION, '0') + def testCpuHardware(self): + info(CPU_HARDWARE) + self.assertNotEqual(CPU_HARDWARE, '') + + if __name__ == '__main__': unittest.main() \ No newline at end of file From d439e52697a12ba0406d756d342a94476e0c5e34 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 7 Jul 2017 12:50:17 -0600 Subject: [PATCH 027/129] Add Tinker Board I2C bus support. --- myDevices/devices/bus.py | 63 +++++++++++++++++++++++++--------------- myDevices/devices/i2c.py | 4 +-- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index 23f91f7..620353a 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -18,31 +18,46 @@ from myDevices.utils.logger import debug, info from myDevices.system.version import OS_VERSION, OS_JESSIE, OS_WHEEZY +from myDevices.system.hardware import Hardware + +if Hardware().getModel() != 'Tinker Board': + BUSLIST = { + "I2C": { + "enabled": False, + "gpio": {0:"SDA", 1:"SCL", 2:"SDA", 3:"SCL"}, + "modules": ["i2c-bcm2708", "i2c-dev"] + }, + + "SPI": { + "enabled": False, + "gpio": {7:"CE1", 8:"CE0", 9:"MISO", 10:"MOSI", 11:"SCLK"}, + "modules": ["spi-bcm2708" if OS_VERSION == OS_WHEEZY else "spi-bcm2835"] + }, + + "UART": { + "enabled": False, + "gpio": {14:"TX", 15:"RX"} + }, + + "ONEWIRE": { + "enabled": False, + "gpio": {4:"DATA"}, + "modules": ["w1-gpio"], + "wait": 2} + } +else: + # Tinker Board only supports I2C and SPI for now. These are enabled by default and + # don't need to load any modules. + BUSLIST = { + "I2C": { + "enabled": True, + }, + + "SPI": { + "enabled": True, + } + } -BUSLIST = { - "I2C": { - "enabled": False, - "gpio": {0:"SDA", 1:"SCL", 2:"SDA", 3:"SCL"}, - "modules": ["i2c-bcm2708", "i2c-dev"] - }, - - "SPI": { - "enabled": False, - "gpio": {7:"CE1", 8:"CE0", 9:"MISO", 10:"MOSI", 11:"SCLK"}, - "modules": ["spi-bcm2708" if OS_VERSION == OS_WHEEZY else "spi-bcm2835"] - }, - - "UART": { - "enabled": False, - "gpio": {14:"TX", 15:"RX"} - }, - - "ONEWIRE": { - "enabled": False, - "gpio": {4:"DATA"}, - "modules": ["w1-gpio"], - "wait": 2} -} def loadModule(module): subprocess.call(["sudo", "modprobe", module]) diff --git a/myDevices/devices/i2c.py b/myDevices/devices/i2c.py index 8bfb1e6..cad866b 100644 --- a/myDevices/devices/i2c.py +++ b/myDevices/devices/i2c.py @@ -14,7 +14,7 @@ import fcntl -from myDevices.system.hardware import BOARD_REVISION +from myDevices.system.hardware import BOARD_REVISION, Hardware from myDevices.devices.bus import Bus # /dev/i2c-X ioctl commands. The ioctl's parameter is always an @@ -52,7 +52,7 @@ def __init__(self, slave): raise Exception("SLAVE_ADDRESS_USED") self.channel = 0 - if BOARD_REVISION > 1: + if BOARD_REVISION > 1 or Hardware().getModel() == 'Tinker Board': self.channel = 1 Bus.__init__(self, "I2C", "/dev/i2c-%d" % self.channel) From 3ead66ab9f3adb6906b4a994e3facac81d9d864e Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 7 Jul 2017 16:24:33 -0600 Subject: [PATCH 028/129] Fix bus info test for Tinker Board buses. --- myDevices/test/sensors_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index 53317b9..7d90135 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -35,7 +35,7 @@ def testBusInfo(self): # print('{}: {} | {}'.format(gpio, bus['GPIO'][gpio]['function'], port_use[RPi.GPIO.gpio_function(gpio)])) # except ValueError as error: # print('{}: {}'.format(error, gpio)) - self.assertEqual(set(bus.keys()), set(['GpioMap', 'SPI', 'GPIO', 'ONEWIRE', 'I2C', 'UART'])) + self.assertTrue(bus) def testSetFunction(self): self.setChannelFunction(GPIO.instance.pins[0], 'IN') From e7939fef132b68129f326dec1e1933cc1e68e281 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 7 Jul 2017 17:29:37 -0600 Subject: [PATCH 029/129] Add user to the i2c group during install. --- setup.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index bea428e..26c39ea 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup, Extension import os import pwd +import grp classifiers = ['Development Status :: 1 - Alpha', 'Operating System :: POSIX :: Linux', @@ -12,21 +13,23 @@ 'Topic :: System :: Hardware'] try: - # Try to install under the cayenne user, if it exists. - user = pwd.getpwnam('cayenne') - user_id = user.pw_uid - group_id = user.pw_gid + # Try to install under the cayenne user, if it exists. + username = 'cayenne' + user = pwd.getpwnam(username) + user_id = user.pw_uid + group_id = user.pw_gid except KeyError: - # Otherwise install under the user that ran sudo. - user_id = int(os.environ['SUDO_UID']) - group_id = int(os.environ['SUDO_GID']) + # Otherwise install under the user that ran sudo. + user_id = int(os.environ['SUDO_UID']) + group_id = int(os.environ['SUDO_GID']) + username = pwd.getpwuid(user_id).pw_name directories = ('/etc/myDevices', '/var/log/myDevices', '/var/run/myDevices') for directory in directories: - try: - os.makedirs(directory) - except FileExistsError: - pass - os.chown(directory, user_id, group_id) + try: + os.makedirs(directory) + except FileExistsError: + pass + os.chown(directory, user_id, group_id) setup(name = 'myDevices', version = '0.2.1', @@ -44,3 +47,9 @@ ) os.chmod('/etc/myDevices/scripts/config.sh', 0o0755) + +# Add user to the i2c group if it isn't already a member +groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem] +if not 'i2c' in groups: + os.system('usermod -a -G i2c {}'.format(username)) + print('\nYou may need to re-login in order to use I2C devices') From fd2eb60ebd7d28930689e1cea462d0432e7a91a3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 7 Jul 2017 18:00:14 -0600 Subject: [PATCH 030/129] Add conf file to create /var/run/myDevices at boot. --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 26c39ea..df4ebd8 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,10 @@ os.chmod('/etc/myDevices/scripts/config.sh', 0o0755) +# Add conf file to create /var/run/myDevices at boot +with open('/usr/lib/tmpfiles.d/cayenne.conf', 'w') as tmpfile: + tmpfile.write('d /run/myDevices 0744 {0} {0} -\n'.format(username)) + # Add user to the i2c group if it isn't already a member groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem] if not 'i2c' in groups: From 10df6a1a96c3eee83ca75d9eebe38be4f1fa1945 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 7 Jul 2017 18:09:39 -0600 Subject: [PATCH 031/129] Update sensor tests to work with Tinker Board. --- myDevices/test/sensors_test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index 7d90135..672402c 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -38,17 +38,17 @@ def testBusInfo(self): self.assertTrue(bus) def testSetFunction(self): - self.setChannelFunction(GPIO.instance.pins[0], 'IN') - self.setChannelFunction(GPIO.instance.pins[0], 'OUT') + self.setChannelFunction(GPIO.instance.pins[7], 'IN') + self.setChannelFunction(GPIO.instance.pins[7], 'OUT') def testSetValue(self): - self.setChannelFunction(GPIO.instance.pins[0], 'OUT') - self.setChannelValue(GPIO.instance.pins[0], 1) - self.setChannelValue(GPIO.instance.pins[0], 0) + self.setChannelFunction(GPIO.instance.pins[7], 'OUT') + self.setChannelValue(GPIO.instance.pins[7], 1) + self.setChannelValue(GPIO.instance.pins[7], 0) def testSensors(self): #Test adding a sensor - channel = GPIO.instance.pins[4] + channel = GPIO.instance.pins[8] testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} compareKeys = ('args', 'description', 'device') retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) @@ -71,7 +71,7 @@ def testSensors(self): self.assertNotIn(testSensor['name'], deviceNames) def testSensorInfo(self): - channel = GPIO.instance.pins[6] + channel = GPIO.instance.pins[9] sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'test_actuator'}, # 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, # 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} From d95f98534fa48b7555e8cfd425e5a340653c2c90 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 11 Jul 2017 18:02:01 -0600 Subject: [PATCH 032/129] Add SPI support for Tinker Board. --- myDevices/devices/spi.py | 13 ++++++++----- setup.py | 24 +++++++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/myDevices/devices/spi.py b/myDevices/devices/spi.py index e4a51c0..bf42a26 100644 --- a/myDevices/devices/spi.py +++ b/myDevices/devices/spi.py @@ -19,6 +19,7 @@ from sys import version_info from myDevices.devices.bus import Bus +from myDevices.system.hardware import Hardware # from spi/spidev.h _IOC_NRBITS = 8 @@ -84,11 +85,14 @@ def SPI_IOC_MESSAGE(count): SPI_IOC_WR_MAX_SPEED_HZ = _IOW(SPI_IOC_MAGIC, 4, 4) class SPI(Bus): - def __init__(self, chip=0, mode=0, bits=8, speed=0): - Bus.__init__(self, "SPI", "/dev/spidev0.%d" % chip) + def __init__(self, chip=0, mode=0, bits=8, speed=0, init=True): + bus = 0 + if Hardware().getModel() == 'Tinker Board': + bus = 2 + Bus.__init__(self, "SPI", "/dev/spidev%d.%d" % (bus, chip)) self.chip = chip - val8 = array.array('B', [0]) + val8 = array.array('B', [0]) val8[0] = mode if fcntl.ioctl(self.fd, SPI_IOC_WR_MODE, val8): raise Exception("Cannot write SPI Mode") @@ -114,7 +118,7 @@ def __init__(self, chip=0, mode=0, bits=8, speed=0): raise Exception("Cannot read SPI Max speed") self.speed = struct.unpack('I', val32)[0] assert((self.speed == speed) or (speed == 0)) - + def __str__(self): return "SPI(chip=%d, mode=%d, speed=%dHz)" % (self.chip, self.mode, self.speed) @@ -142,4 +146,3 @@ def xfer(self, txbuff=None): fcntl.ioctl(self.fd, SPI_IOC_MESSAGE(len(data)), data) _rxbuff = ctypes.string_at(_rxptr, length) return bytearray(_rxbuff) - \ No newline at end of file diff --git a/setup.py b/setup.py index df4ebd8..e8e3844 100644 --- a/setup.py +++ b/setup.py @@ -51,9 +51,23 @@ # Add conf file to create /var/run/myDevices at boot with open('/usr/lib/tmpfiles.d/cayenne.conf', 'w') as tmpfile: tmpfile.write('d /run/myDevices 0744 {0} {0} -\n'.format(username)) - + +# Add spi group if it doesn't exist +relogin = False +all_groups = [g.gr_name for g in grp.getgrall()] +if not 'spi' in all_groups: + os.system('groupadd -f -f spi') + os.system('adduser {} spi'.format(username)) + with open('/etc/udev/rules.d/99-com.rules', 'w') as spirules: + spirules.write('SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"\n') + os.system('udevadm control --reload-rules && udevadm trigger') + relogin = True + # Add user to the i2c group if it isn't already a member -groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem] -if not 'i2c' in groups: - os.system('usermod -a -G i2c {}'.format(username)) - print('\nYou may need to re-login in order to use I2C devices') +user_groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem] +if not 'i2c' in user_groups: + os.system('adduser {} i2c'.format(username)) + relogin = True + +if relogin: + print('\nYou may need to re-login in order to use I2C or SPI devices') From 59de5acc906ee4f6499199e453804524fbb5bdf9 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 12 Jul 2017 17:25:13 -0600 Subject: [PATCH 033/129] Disable system config changes on the Tinker Board. --- myDevices/cloud/client.py | 6 ++--- .../{raspiconfig.py => systemconfig.py} | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) rename myDevices/system/{raspiconfig.py => systemconfig.py} (75%) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 818a3f0..73eb728 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -21,7 +21,7 @@ from myDevices.cloud.scheduler import SchedulerEngine from myDevices.cloud.download_speed import DownloadSpeed from myDevices.cloud.updater import Updater -from myDevices.system.raspiconfig import RaspiConfig +from myDevices.system.systemconfig import SystemConfig from myDevices.utils.daemon import Daemon from myDevices.utils.threadpool import ThreadPool from myDevices.utils.history import History @@ -401,7 +401,7 @@ def BuildPT_SYSTEM_INFO(self): raspberryValue['SystemInfo'] = self.sensorsClient.currentSystemInfo raspberryValue['SensorsInfo'] = self.sensorsClient.currentSensorsInfo raspberryValue['BusInfo'] = self.sensorsClient.currentBusInfo - raspberryValue['OsSettings'] = RaspiConfig.getConfig() + raspberryValue['OsSettings'] = SystemConfig.getConfig() raspberryValue['NetworkId'] = WifiManager.Network.GetNetworkId() raspberryValue['WifiStatus'] = self.wifiManager.GetStatus() try: @@ -925,7 +925,7 @@ def ProcessDeviceCommand(self, messageObject): try: config_id = parameters["id"] arguments = parameters["arguments"] - (retValue, output) = RaspiConfig.ExecuteConfigCommand(config_id, arguments) + (retValue, output) = SystemConfig.ExecuteConfigCommand(config_id, arguments) data["Output"] = output retValue = str(retValue) except: diff --git a/myDevices/system/raspiconfig.py b/myDevices/system/systemconfig.py similarity index 75% rename from myDevices/system/raspiconfig.py rename to myDevices/system/systemconfig.py index 3b28351..e55be0e 100644 --- a/myDevices/system/raspiconfig.py +++ b/myDevices/system/systemconfig.py @@ -1,14 +1,15 @@ """ -This module provides a class for modifying Raspberry Pi configuration settings. +This module provides a class for modifying system configuration settings. """ +from time import sleep from myDevices.utils.logger import exception, info, warn, error, debug from myDevices.utils.subprocess import executeCommand -from time import sleep from myDevices.utils.threadpool import ThreadPool +from myDevices.system.hardware import Hardware CUSTOM_CONFIG_SCRIPT = "/etc/myDevices/scripts/config.sh" -class RaspiConfig: +class SystemConfig: """Class for modifying configuration settings""" @staticmethod @@ -29,14 +30,16 @@ def ExecuteConfigCommand(config_id, parameters): config_id: Id of command to run parameters: Parameters to use when executing command """ - debug('RaspiConfig::ExecuteConfigCommand') + if Hardware().getModel() == 'Tinker Board': + return (1, 'Not supported') + debug('SystemConfig::ExecuteConfigCommand') if config_id == 0: - return RaspiConfig.ExpandRootfs() + return SystemConfig.ExpandRootfs() command = "sudo " + CUSTOM_CONFIG_SCRIPT + " " + str(config_id) + " " + str(parameters) (output, returnCode) = executeCommand(command) debug('ExecuteConfigCommand '+ str(config_id) + ' args: ' + str(parameters) + ' retCode: ' + str(returnCode) + ' output: ' + output ) if "reboot required" in output: - ThreadPool.Submit(RaspiConfig.RestartService) + ThreadPool.Submit(SystemConfig.RestartService) return (returnCode, output) @staticmethod @@ -50,8 +53,10 @@ def RestartService(): def getConfig(): """Return dict containing configuration settings""" configItem = {} + if Hardware().getModel() == 'Tinker Board': + return configItem try: - (returnCode, output) = RaspiConfig.ExecuteConfigCommand(17, '') + (returnCode, output) = SystemConfig.ExecuteConfigCommand(17, '') if output: values = output.strip().split(' ') configItem['Camera'] = {} @@ -64,19 +69,19 @@ def getConfig(): exception('Camera config') try: - (returnCode, output) = RaspiConfig.ExecuteConfigCommand(10, '') + (returnCode, output) = SystemConfig.ExecuteConfigCommand(10, '') if output: configItem['DeviceTree'] = int(output.strip()) del output - (returnCode, output) = RaspiConfig.ExecuteConfigCommand(18, '') + (returnCode, output) = SystemConfig.ExecuteConfigCommand(18, '') if output: configItem['Serial'] = int(output.strip()) del output - (returnCode, output) = RaspiConfig.ExecuteConfigCommand(20, '') + (returnCode, output) = SystemConfig.ExecuteConfigCommand(20, '') if output: configItem['OneWire'] = int(output.strip()) del output except: exception('Camera config') - info('RaspiConfig: {}'.format(configItem)) + info('SystemConfig: {}'.format(configItem)) return configItem \ No newline at end of file From a16d9c631052dca5a24edc93722a276b7da04b57 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 17 Jul 2017 17:46:47 -0600 Subject: [PATCH 034/129] Pipe stderr data when executing commands so it is not displayed in the console. --- myDevices/utils/subprocess.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/myDevices/utils/subprocess.py b/myDevices/utils/subprocess.py index 9768bb7..f26fe35 100644 --- a/myDevices/utils/subprocess.py +++ b/myDevices/utils/subprocess.py @@ -22,16 +22,14 @@ def executeCommand(command, increaseMemoryLimit=False): setLimit = None if increaseMemoryLimit: setLimit = setMemoryLimits - process = Popen(command, stdout=PIPE, shell=True, preexec_fn=setLimit) - processOutput = process.communicate() + process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, preexec_fn=setLimit) + (stdout_data, stderr_data) = process.communicate() returncode = process.wait() returncode = process.returncode - debug('executeCommand: ' + str(processOutput)) - if processOutput and processOutput[0]: - output = str(processOutput[0].decode('utf-8')) - processOutput = None + debug('executeCommand: stdout_data {}, stderr_data {}'.format(stdout_data, stderr_data)) + if stdout_data: + output = stdout_data.decode('utf-8') + stdout_data = None except: exception('executeCommand failed: ' + command) - debug('executeCommand: ' + command + ' ' + str(output)) - retOut = str(output) - return (retOut, returncode) + return (output, returncode) From 590a9fe0819190761345a2958877466ee5a651f8 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 17 Jul 2017 17:48:29 -0600 Subject: [PATCH 035/129] Check board type when Installing Tinker Board specific items. Install the ASUS.GPIO library if it is not installed. --- setup.py | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index e8e3844..aea8b8f 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,8 @@ import os import pwd import grp +from myDevices.system.hardware import Hardware + classifiers = ['Development Status :: 1 - Alpha', 'Operating System :: POSIX :: Linux', @@ -52,22 +54,45 @@ with open('/usr/lib/tmpfiles.d/cayenne.conf', 'w') as tmpfile: tmpfile.write('d /run/myDevices 0744 {0} {0} -\n'.format(username)) -# Add spi group if it doesn't exist relogin = False -all_groups = [g.gr_name for g in grp.getgrall()] -if not 'spi' in all_groups: - os.system('groupadd -f -f spi') - os.system('adduser {} spi'.format(username)) - with open('/etc/udev/rules.d/99-com.rules', 'w') as spirules: - spirules.write('SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"\n') - os.system('udevadm control --reload-rules && udevadm trigger') - relogin = True - # Add user to the i2c group if it isn't already a member user_groups = [g.gr_name for g in grp.getgrall() if username in g.gr_mem] if not 'i2c' in user_groups: os.system('adduser {} i2c'.format(username)) relogin = True +if Hardware().getModel() == 'Tinker Board': + # Add spi group if it doesn't exist + all_groups = [g.gr_name for g in grp.getgrall()] + if not 'spi' in all_groups: + os.system('groupadd -f -f spi') + os.system('adduser {} spi'.format(username)) + with open('/etc/udev/rules.d/99-com.rules', 'w') as spirules: + spirules.write('SUBSYSTEM=="spidev", GROUP="spi", MODE="0660"\n') + os.system('udevadm control --reload-rules && udevadm trigger') + relogin = True + # Install GPIO library if it doesn't exist + try: + import ASUS.GPIO + except: + current_dir = os.getcwd() + try: + TEMP_FOLDER = '/tmp/GPIO_API_for_Python' + GPIO_API_ZIP = TEMP_FOLDER + '.zip' + import urllib.request + print('Downloading ASUS.GPIO library') + urllib.request.urlretrieve('http://dlcdnet.asus.com/pub/ASUS/mb/Linux/Tinker_Board_2GB/GPIO_API_for_Python.zip', GPIO_API_ZIP) + import zipfile + with zipfile.ZipFile(GPIO_API_ZIP, 'r') as lib_zip: + lib_zip.extractall(TEMP_FOLDER) + os.chdir(TEMP_FOLDER) + import distutils.core + print('Installing ASUS.GPIO library') + distutils.core.run_setup(TEMP_FOLDER + '/setup.py', ['install']) + except Exception as ex: + print('Error installing ASUS.GPIO library: {}'.format(ex)) + finally: + os.chdir(current_dir) + if relogin: print('\nYou may need to re-login in order to use I2C or SPI devices') From ea9d122df1ade916a2c4266f99ca8a8c30190638 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 31 Jul 2017 16:26:36 -0600 Subject: [PATCH 036/129] Do not show 1-wire errors when 1-wire devices are not supported by a board. --- myDevices/devices/manager.py | 2 -- myDevices/devices/onewire.py | 25 ++++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/myDevices/devices/manager.py b/myDevices/devices/manager.py index d4ca9de..a216a8c 100644 --- a/myDevices/devices/manager.py +++ b/myDevices/devices/manager.py @@ -29,8 +29,6 @@ def deviceDetector(): except Exception as e: logger.error("Device detector: %s" % e) - sleep(5) - def findDeviceClass(name): for package in PACKAGES: if hasattr(package, name): diff --git a/myDevices/devices/onewire.py b/myDevices/devices/onewire.py index a03a4f8..40f1246 100644 --- a/myDevices/devices/onewire.py +++ b/myDevices/devices/onewire.py @@ -74,7 +74,7 @@ def deviceList(self): devices.append(line) else: devices = lines - return devices; + return devices def read(self): data = "" @@ -90,15 +90,18 @@ def detectOneWireDevices(): devices = [] debug('detectOneWireDevices') slaveList = "/sys/bus/w1/devices/w1_bus_master1/w1_master_slaves" - with open(slaveList) as f: - lines = f.read().split("\n") - for line in lines: - debug(line) - if (len(line) > 0) and ('-' in line): - (family, addr) = line.split("-") - if family in FAMILIES: - device = {'name': addr, 'description': FAMILIES[family], 'device': FAMILIES[family], 'args': {'slave': line}} - debug(str(device)) - devices.append(device) + try: + with open(slaveList) as f: + lines = f.read().split("\n") + for line in lines: + debug(line) + if (len(line) > 0) and ('-' in line): + (family, addr) = line.split("-") + if family in FAMILIES: + device = {'name': addr, 'description': FAMILIES[family], 'device': FAMILIES[family], 'args': {'slave': line}} + debug(str(device)) + devices.append(device) + except FileNotFoundError as err: + debug('Error detecting 1-wire devices: {}'.format(err)) return devices From 59c24cbc5797d68d012c49c073761c67da003b0b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 1 Aug 2017 17:43:21 -0600 Subject: [PATCH 037/129] Fix PiFace device to allow adding sensors. --- myDevices/devices/manager.py | 2 +- myDevices/devices/shield/piface.py | 69 +++--------------------------- myDevices/sensors/sensors.py | 2 - 3 files changed, 7 insertions(+), 66 deletions(-) diff --git a/myDevices/devices/manager.py b/myDevices/devices/manager.py index d4ca9de..a37e4c1 100644 --- a/myDevices/devices/manager.py +++ b/myDevices/devices/manager.py @@ -161,7 +161,7 @@ def addDevice(name, device, description, args, origin): def addDeviceConf(devices, origin): for (name, params) in devices: values = params.split(" ") - driver = values[0]; + driver = values[0] description = name args = {} i = 1 diff --git a/myDevices/devices/shield/piface.py b/myDevices/devices/shield/piface.py index 81bc23e..31c5641 100644 --- a/myDevices/devices/shield/piface.py +++ b/myDevices/devices/shield/piface.py @@ -17,72 +17,15 @@ from myDevices.decorators.rest import request, response -class PiFaceDigital(): +class PiFaceDigital(MCP23S17): def __init__(self, board=0): - mcp = MCP23S17(0, 0x20+toint(board)) - mcp.writeRegister(mcp.getAddress(mcp.IODIR, 0), 0x00) # Port A as output - mcp.writeRegister(mcp.getAddress(mcp.IODIR, 8), 0xFF) # Port B as input - mcp.writeRegister(mcp.getAddress(mcp.GPPU, 0), 0x00) # Port A PU OFF - mcp.writeRegister(mcp.getAddress(mcp.GPPU, 8), 0xFF) # Port B PU ON - self.mcp = mcp self.board = toint(board) + MCP23S17.__init__(self, 0, 0x20 + toint(board)) + self.writeRegister(self.getAddress(self.IODIR, 0), 0x00) # Port A as output + self.writeRegister(self.getAddress(self.IODIR, 8), 0xFF) # Port B as input + self.writeRegister(self.getAddress(self.GPPU, 0), 0x00) # Port A PU OFF + self.writeRegister(self.getAddress(self.GPPU, 8), 0xFF) # Port B PU ON def __str__(self): return "PiFaceDigital(%d)" % self.board - def __family__(self): - return "GPIOPort" - - def checkChannel(self, channel): -# if not channel in range(8): - if not channel in range(16): - raise ValueError("Channel %d invalid" % channel) - - #@request("GET", "%(channel)d/value") - @response("%d") - def digitalRead(self, channel): - self.checkChannel(channel) -# return not self.mcp.digitalRead(channel+8) - return self.mcp.digitalRead(channel) - - #@request("POST", "%(channel)d/value/%(value)d") - @response("%d") - def digitalWrite(self, channel, value): - self.checkChannel(channel) - return self.mcp.digitalWrite(channel, value) - -# #@request("GET", "digital/output/%(channel)d") -# @response("%d") -# def digitalReadOutput(self, channel): -# self.checkChannel(channel) -# return self.mcp.digitalRead(channel) - -# #@request("GET", "digital/*") -# @response(contentType=M_JSON) -# def readAll(self): -# inputs = {} -# outputs = {} -# for i in range(8): -# inputs[i] = self.digitalRead(i) -# outputs[i] = self.digitalReadOutput(i) -# return {"input": inputs, "output": outputs} - - #@request("GET", "*") - @response(contentType=M_JSON) - def readAll(self): -# inputs = {} -# outputs = {} -# for i in range(8): -# inputs[i] = self.digitalRead(i) -# outputs[i] = self.digitalReadOutput(i) -# return {"input": inputs, "output": outputs} - - values = {} - for i in range(16): - values[i] = {"function": self.mcp.getFunctionString(i), "value": int(self.mcp.digitalRead(i))} - return values - - #@request("GET", "count") - @response("%d") - def digitalCount(self): - return 16 diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index d0603b9..f639080 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -269,8 +269,6 @@ def SensorsInfo(self): if value['type'] == 'Humidity': value['float']=self.CallDeviceFunction(sensor.getHumidity) value['percent']=self.CallDeviceFunction(sensor.getHumidityPercent) - if value['type'] == 'PiFaceDigital': - value['all'] = self.CallDeviceFunction(sensor.readAll) if value['type'] in ('DigitalSensor', 'DigitalActuator'): value['value'] = self.CallDeviceFunction(sensor.read) if value['type'] == 'GPIOPort': From 008a5029aa3efe6f8a4fa9344547f04ec4b26a3b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 2 Aug 2017 10:59:22 -0600 Subject: [PATCH 038/129] Convert board value to int only once. --- myDevices/devices/shield/piface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/devices/shield/piface.py b/myDevices/devices/shield/piface.py index 31c5641..722fed5 100644 --- a/myDevices/devices/shield/piface.py +++ b/myDevices/devices/shield/piface.py @@ -20,7 +20,7 @@ class PiFaceDigital(MCP23S17): def __init__(self, board=0): self.board = toint(board) - MCP23S17.__init__(self, 0, 0x20 + toint(board)) + MCP23S17.__init__(self, 0, 0x20 + self.board) self.writeRegister(self.getAddress(self.IODIR, 0), 0x00) # Port A as output self.writeRegister(self.getAddress(self.IODIR, 8), 0xFF) # Port B as input self.writeRegister(self.getAddress(self.GPPU, 0), 0x00) # Port A PU OFF From 3a93e6c6f63e62b3dc90a10d334581a372553136 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 8 Aug 2017 11:51:25 -0600 Subject: [PATCH 039/129] Update Python version requirement. --- README.rst | 2 +- myDevices-test.tar.gz | Bin 0 -> 79709 bytes package.sh | 8 ++++++++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 myDevices-test.tar.gz create mode 100644 package.sh diff --git a/README.rst b/README.rst index edc0417..d6fd20c 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ The Cayenne agent is a full featured client for the `Cayenne IoT project builder ************ Requirements ************ -* `Python 3 `_. +* `Python 3.3 or newer `_. * pip3 - Python 3 package manager. This should already be available in Python 3.4+ and above. If it isn't in can be installed using the system package manager. Via `apt-get` this would be: :: diff --git a/myDevices-test.tar.gz b/myDevices-test.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..ee7c4601290b6350d2b6259b1c7f951266a50cf7 GIT binary patch literal 79709 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%0DiMDAKqQ^{LyW~IKb-Pi2s|To!JS-r zkNixh1B}yvR>L=`bR8>TS+Z;$K zWsHZHROgC`jlZ-21nSS(=i|nSBub{Uayw9tdZfa)7{a;haC?BE$W9dFRYf>OO6*~o zrCDJ=a>JEY1Hzv19~OM^Lbf3u|2?iD&KzkTjk|%Fe*0Hln8Bn&aYiHOZ7vWoi9N=;6}-Lm+e=JQ`)2Wo*?I zx!@DZWv<+IItzR7c@7JgCtgTQ$DS2`d0FINaQ!0ZJG<8L#I<(taAsCwS1bnKx4e4F z32@OyFJQ427*(9K!6B%Z_sVf~ChyDv(}}mNg9nQI(Sy;~k7;XVLZh&!ls|K*1?u=p zWr;nrb{o{Pl6`&(2STglwxn*eK;54yw});YWO7f!05;cvj4fHZ9I9!o!~&Td#%X4g z3AxJj7%7^W&V;+#iN2z~mZ}dcB6J3|2?mhH4BIlJCq_wkUu;v=>P<>Hj;SwR7zY1<7jSmMW&URL#8uqgofNy;-KUhostW8F zG0O;k2O>VDm|+RKOQu=*rHr?4`CC^jD_*@Ud`hk^mc7+f6jMga@-KY1$T+4uDe{eO zx&xz>_DZ9VI#?=k44&5MqEt#HirV6LIj3ierzLa7E~?zQ1f{L+sh_2P<9n0Li^c<@ z&?E~=xOd4V?8nh(4670z9qvH%1Y4g()8tV@Jd(YFhfR8@x02geM>INwR{%)5GUW?c zz}q5#*7n#f?sGD*Lp4|-UZ!gCs4%mFI7Yp)!tCD_e=sPR0*H0+h$+e<41{8)3jaBB zm!|x)4zHMcrcVXz;92EpA3NDMKS0br-3OY6-NlfO;( z)6TQlMVoGeU2DZ=G3 zz-H&K3T>X}LKL$?k-1Q(`0@lW#hg=o^D_k}=fX4f41kD7EI8s3)5EtDJQX@d!?ohA3(u$&64lg^t;b2=)HCh!{+n5`Ba>t!pCB&fCfEGmWDe7$Qni zhY8)%j$47&^hAV{Y_YKbPD5hkaC9>V#C zpuqy`+U_@vOj>|Sj&jLE!#k6EEGareMcTf9O79c7Bh@FDwJUo-Btuy6F?ENy+GjTH z>FH2m0$;=sOHh@}zBrU6U(CQ^eXg31+%LzhKf``ypn)!6|& zN@W9i`>-ZU87ZU=%PXZ-;c2Xr{B}jDVpc@Ys_^e9%Do#jr_*XWw7gP1OAcs}J(0;p zz@w*<74pmc604`>D|?yv@2vQLO5aYG);;Y6Kz96py}4Q6FwcKC*EU|`|6k(cy#HYo zOv6410DcWb7ZYL2qXD~nM@JQ}+rf+2I*Z-?kRJb!*D~DvH5REyBiS0J6Rn`8fDtI7(mR1=sYF*l1AVxmRfJu!{2J&&iSfeD^O@V{7CWcH2bI zV>N?&e>ffl+uQoZg}+$}4_*+`D{gzu&CObKW2;uP?+4^JaBO?;Yt8qY?`yR!N15wc zzaLCuC{=H~-&kMU+*)&#>iXSVYOdZ~uVZVj=8`B7C|7SZU@@w1ILh_$orGzH0sE5Rx#c0^}9OQ^1IyG&5{ydhI9s; z->{Ln2h1L-x2Irb$M&x%*caz`shoTBRg@*dUCFK5q?Hu|oU^uI71hc1(A|WIG%BCT4k>e1${$8auKFOf9s32)hwpY81J~aDuSSb@$9^Of=FSNtY_RRu?S~!Vwo38%o2Tq zeKncZT5zS}@Qs)jw*-J8FH;sGyi0^%&;{*Kn`4K~d8^YHlcrC3Wl z$dp=)CG-mPY#fC~z)DhybBSJyJ+x}11~iI$hBv>Fpa9UY9mde{hlJe!ts}lLh34GHq@xq>f62Rt?g!at+%~h_Zxlu z=Q^#?t2CkMcM+WLZ(m<;ce~rY-gXdd_xmpKX+J954g66i<^E88dX#O)Sul?uh;mxt z=_qvh(H%0v{a2NbonaQ72&6J9b^7voN!E90apIK&#Z*+)mZ6Gl=rU=3=b&?VvVVc9 zBM3q$;LtYh_AiT})7;7`&A>zZR1v>FT@0r7*hb*X2DN+nn?z%qkPy8V>i3&s8EEl4 z4KvXVMmU3T8&{4q8egp&!1R)O4up;lPkzcA$%QeaO;#0$lpsE3`drEiJY$to`*91D zx;`wjMyXgim#M}5{~7x~N#@nBy8i>}c*DH^d!wm-wWOKjzO3;%VIB@?iA|ZOpm<(=q-V>zgqC z>(AE9jL+-%=No^}&G0sf$7F+?m;WCxd)3CjQ3n=yo&PWKS@u@muE6?_`4YE1ic!NS zOK3})Jh1e?#%F2i;x>%EA>HhQacp10UVwg%c<2uXsMN3EoumBPZ2``^oleKzAiT!A zORL`D)EffrV{aA*OHrR+LF+ZE&0_3QOgJ?06NA)9zaZD<1lTv4#en(%<0zrD09a-2 z6doR{cTDIk2kd8?u{x{3`5upBYj63!-Le3N_jFEXq_Jj&KElD#q3j|O zVtaKJ|6GToYBaf7h37>9xmvAyy~vC3I(aXi&iZ|P#9!V~>MfnL7`lQ7-$6eLDrD+R zy<0yX6{ns*Kn;qW!h=qXVJ6ci3gJbG^9CXaljoI_rC31qw)a~tIr%JJ^6VxZe&uCS zT^8V*LFFZ4DIQR?u(~OEN8TpEhDdu^^?UvpbIHp9#QlSvk4G1sv(wX~_Thi*SG*rD zF3xsXDH#f6j4S_J-cFo;+8Mrz)G1phbcwvGu{J7Vg!xNU9b<>b=k43iclb(o9>{Vxh)cQ{L|svvf~B`_bwnM}(I%8!K$f)H z%0P;*dVxg7$Lf5;sn2B#pZO*;X_NQZ4=HpiCXNs8!=TMI;hh`Eb(`X-(^E5VFjH$#XBP_nj*!Yz+6LvR` zPxw)=f<1}Ubv8ins(n~q0jib0S5{@*VCJVq;fFZCV4k>lcx?Vp-({WK{(FKqXpa3? zubc7zjoN1I)&KJ%pEUb#X&F>Ru5)wY$TWCAet=mo^>JK?g)L{~nn!gO$zq!G+ zVX4!ZhSNdN>3DP*7lXIL*{uk#zkpT&!k87PRo_?F@#*y}9Q5c}?Vj-uJoIOJkJKfQ~bW-v$+3vZL_)g>i>O-&)o5U zhFzfS^?$A2Y?}MOoA~E-{eOWEF3o%;`tRu4auJd4@7y;^mr1i^a6GV1(?+$?K66l$Qt+J5FMnICF zCtiO7Y-4tEAkj+*aI@N6TB5{m4BhX)|6X3%%SX<9PT}$LA?h4}erX7t15jE;@??v< z$FT%y9pp7G92SQjQjYl~whMR$lB$&xfTG!I0DX8U)obM{jP(rqRqaJ3XsVo6v|PQ7 zW)ogYf^HQlAV8!Van|ek4^$D0j&c>WF_wC@`3xR!0yrK875U&1=Vc23Yuwcb#1lY1RG6oVW$gRxVi)!<_S;R z4UFaNnyO8s;lpP?ocODfGnar9=eM%4+N`beYtE^O4su9GLRlrvo)p)fKMmCDoW<`7 zb*~o;roONMXii7bz&f$}=(EFdFx{hD#8d|4--rJ6R(uB`0VCiiuK!(s1k>WLz@5wu z&Y6;_z~ZE^5O#W*xw-vF8d5ZV;!IR{z>XZo>|E$pOT?%@2_yscP&xnr#picMdtB(N z4_Nt-cA?&)%p0LI{W`eyzl5|$LJohPgH95j2`L9-EXTL{O?nBapJ>H{^)m#k`}y!zrl* zymcqR5X2NMA0wwQmhL{|R*8zw@D0!`9RiLIK7+SYu5!0pP;?dzl zgj1OWF8&pX#^_y0qN~cf)`5D;8C<12ZL5?tu!d1ZAQ1q*R78j13Lp!&$zZDZ51kk+ zvr#W@)iPKFhQptLPWEtmA?8lJ#!=?SneaTf2dX42E>pIIq832t4kJJgI0MEe8K^I4HlzGg z$7h+2lov(z&9~6wx89qOfuS|(4WPpM5RN#e#fr>hNAx!+=^i*ydhTGUuN8T1G$Dr+ zlmy?(<_ulj3vg>TrZG&$i57S5G>=H|hi@*o6$#QMfkk*Rc0ueMO`x8m!q>n%vJ&yO zsx0N8-e>N3_!&K``*!LXCM*SlY{6u>$tySN6_y^ZgR25NA^6htK3xrP@iKpRLE zC{@xn6hhE$ffU#cC{;;Twz9`E3gFb14;{m=a8@Fo{tYyK7>((J#BT&zCgSK~ZlMC) zq2$JSkA<08A=iegvBN_<)9z`=$)lx15+{U1b*fhdQfWxziBU029f+UIhSwm6qP`S^ zvk~4y`de`#2YoIQkqC;~dPP5ihiQn}0yNXeGVQm3#q4uHQyl=AtuH%QL^ zlA1@-z9SmJH5GUkDN`j8MMW4pe#N_4{26Wa@h5NbYS*b;0ibiS#n2tT_k*n<2rlJ4 z`HD>wz20mj6Q5BXag@SY@ z(BLSk_qC6ckL`x5nv{Crn=~yPOB^wmWY9c7R3VA^f( z4X~2nMEq4QdT+dvGd4s;QJWp0ds**9@73T8K=9U&{i(PaQV0*2{$jJCXmni^>4+@b zYQ3tcm9h740>BZ_TThl%9y)VLW$syygL_qPWUXScU}yJas^g>?G^B`qm_nfR=^czG=BGm`3Xou>B!u2^%qX#Qxu}hT zB07cabaGsAX9_HV@gwjr@0yD>RC!xTP^~0fSGJlL;+aRQ+2L3MhkY_G2>~WM$gQ^k zVsgPuErF!C++L|)N$k{hr_LsoESU}`CkbpWb@lZR)H!!|Qj{+yp-mYFT2L^VI@)&% zQ!eGRo3KOw7A1a;J(o`yJSU|Ha^k!A|qA}E6* z@-&PAKq|ucE)!oYun@YaEIkchgpke{z^#uVh^6oq86;=lvQe1Mg7}$m;GiCX#9h!E zJsTPl*kh=13k>~SWJ|$lY(y?u_mEp)Usa6Sxm1N}5EUvaBzt%)l!6>hn$UQ_4N(Dkb*v z2c>ihB+XcvawL@57^&XD(D!j*xi4&20^dW~nezQI$3DuktQL#YdLDyM-e9W-d`tA! z{}LE*gd3y+-&pKPr-QoyolY<6!dD)s;u)PVk1o+(?Oh1}U=IDN_EyWSLcQ+@Z&^of z5$KssO2s`I&&0n(ypr6^VETz%9Icpq(~j^rah{g$C!K|ZsoP2 z8x<$8F&iZYi!7YU=CWgJ0s~6;?ykC#!uP>P(cOIiqtY1~d25>juXWa{Xj{`MfeLc% z1Bl^p81zB_J9xlTB0?61Q)5$;?pi3?uAeN=lPEKPY!W^rQHjWP{Sn`+6Y&7vk@R$G zY$r!9_4te2JJSjP0o9dLkW`;ZhkdsB4PuiKj=b4?n-g82LgvLGv|+fOnR}e=s|9@j zS5V)i6k_u|-;%m%u zz%dVz4bdTD0T2bXMZ=w}ua>E-682rQK(C!fqxR=;Jm#oWlHHGlP6kQj3cX0D*+#Fn zs+5dfWkm|5v)^J>wyMHc)_$k$;;kqfS8w0`CVi6IUj6s7xO7)aVS%rMUbale z>jh_1COlXqz*OwR^W97(KFiv;TK0DEgsArx)gZ7hZa`<91{gHwPeS#gfn{&}FaQf{ z8)JWveyvZSVq1E5!fWBkE6ILVoLjui^;QwKaz&b-6=AMbIKSp*Y+9D`Z7VFcz_3Bi z4IsfIbvf48A7Bxkt<4Yu5L2}@(X_+0#))>hZwBf*W-X0k4t`JBGom3jhpg z%GDEQy?4Sa30-2m)rqw1<-;2am%D_l!uIwBlE-F7pZeLEMnHJR-R{9VhOv-wo4xoC@b zmAP0NQ(0YNTKWZHB z6~3R^s$rGxBoaTJSg?Z+2`(AkG?7leyE)87PC4l~XCxE!Q99T6`XD_&-X@>Qy4K`K zb|g|Y5KFsi**gq0Ch!QE2UjQyE3qHwx^#)}(m7rwJ7=zXVmr%uV^Wy%MsEGM-WfYG z_$jB-LLuEqcHTu1%vQl)gDC6;_qvoYF5|{x9sd?|s-&K&pjG+vlC_Q~wZFq)MO2v*7G`CSvM`#u`6iDw{-e$&P_)ZleNwu=T+F;b&u{Vn-N3JyLQ|%Ol2g&q?SQ5UF`*|O19dNh0@FD3-4N)Bj^+!3s^zR-cc|u#!#Bv zjduaF_~R-5iw;h4gowGJ_{IjEF@Z)+lH>`ECi(O|u=Bj0uef_IK1<%xGG#yNM`*&b z%cUq^5{vA-jV9CXY#LLP9d;g%MB(Lj5!CBazBSKJ4taSZjDCA*iRXFfB*$G_%4x9j z5Fw|Sp zPf;*j=tM+nYiTDTQX6F8m<73Ho`p-AS?CPhg6H4t6PV&C+~beH5g2tlEXVOT8xQ3q z+~OzT2!e77&OH65933#mL`P&v4htSeb00%{mLq6SIDUo>eLRB3$R4Q&(8z1P$RRX3 ze8vE&dqHk8*(5QKek~>Ez7k^PIPYb*$AJit`nenfbm}=T2Uzof&yseYM}fT1IiA!R zpX??rS!ZyP#uhkxliHFBnh6*#V^kBH*?`=dP7dAlddQsd>>4wMP5FCI9%)YKNy?)L zNmycQrmTju_d4j(IT{WE=K+eAzXg+Mg^m?aCFb+dA{X9?Dl0rNhMY-C(1Ix8t*#2! z*D|SOFjr9KVV*M$IQ#ns3PaVpz(1Dm1SleoLNgpD{^%wsVOyL;<;yyYHD`(}F4P(X z<*))LjPjCADs}T*iMEhts?r*UuyEZw zA1x36xB8^!e+fo$GzFFJcF!TR!Hr8JA z|GdQKxl(XImA}R!A-9?cgbvEHP)hcaGQ`ScfPOEo^k%~`PX@yB!73T>Smx!5cYYMX zB4=P5$xPod*fID>&XuaozaSY{i+ zlq#UWg&6nXvrP9;)W#c>h+vq|VUK>9#kzxN)~oiehyKV{#$)gLm_2ZSP3~;6Gpe)V zw|;!9U?1Q5b^by@oxj}mN1(#AK~dnjAVoJB1w?*a>?wS8r*q+190kbZowKvUlON2o zJ^uH4Cg~39UH9|OVDMoUSG*4&+ee4(i?nLJ@Ftx4gX+yVjAZUae;R%XemFZkMf~t@ zN^MVOig8laC?)&@?%rY&T3PhC^Vjz2iDQ(QokG`riG23-hZ!{5C{ZB3kSHcKQ_aTeeK zM!+Dv2=f#1J0OzBsWmyBT($LA=@3*5bpYA8^YDotZ%4oOO&N~(Jm^n?_;wdFplhB? zDg=u4Bn;4@rM2gvx8C3dopx?{1@XAUn=7~m9HDYNj;uR|m3eCN0!_yj%{}E6pJ$jx zl+B4lk-4IPB?}eL{40S~pka8-Z&tQJ;(CJ15T9$M0-!uq%CUPcklIp+R@-1mclmC* zgGgGqBCl^m1!IB@u*SlX`+eG&UqYqJ+7({xXBr0E0XRGz)0=M1G@;GH$-QWsb==f)_!MB zarI}?WrG+l(d866T6ESjTN&kD4y7sEis7@CS1g<5nPXjH-eftgii_v0`V8x6{1~K@ znTt^PE3-QKMlRS%66smjH)KQ!jl3ykd0?)B2A2ioyNqyckE7WH*Fd6%7Wc=te^dIS z&jaVBplT?lh{F`fyLV%m-1Fn{HE5s@*St=ckmIz)AR8VOleQBV`IGGmTQ_Gj0|Pc{Q5kodjTX?@2}4kq*7IdXNZyFqGrFb;IYRhT99 zWyheELms6@lxV=UIwc1WxtqK`rv-q8Se#QXExvA+6xK>bmsiJaeG2@S`*)h0%-e&c zN){PiYQ{TtMN&Vhg2y?|mLlu8-G$Pq%_*36$!~JMZXu8G!c#05o?YPa!)5PLGVp-M zB^S%4m=;W%5>o7v_gvt+qKggO5MwTkD7qf+nHM;cC832yJ4Ur^C})a>Yb^#=W6|Rq zQ>0vCV7LRn+CT1ecKt!Ol%k}Uk@p=X9ZI`nLw&gydBXr!ed`T9Ra9{T$|pa6ih6HG zl$Qeo_xzNp{TwyYCut6=M8U}DPL{aHfSxi2DeI;cTP&gLa`UpNz@kg7HEi7=9*3Zt zm5;HxM=>+bbO%#B&urHRGlQ)%4nhPE3TralBkxTF~3mkU<_hk=zmCs$cc$+CL%4wdrMshi z%24TE)$F2f^30!fNm*;&xm;EAI5>~RpXJxpTv~P-XSHam^*0LcbkfKlc-d1HH}g@} z0HnA&UG8zMo~*_LKK6wzhnqDfO%g+|XyGT#y5c*WL&3e<^mr7xbvL?tz%zpf)M-`m zv@E}zfb3bO5>_l`4D)pIGo`zseN&M~*dS;+0Xcr0Zm4K&3jxGIIZyQ#-RY48xDE+0 z!mMo3q9deRg{PmvQVE8rS>ZmUx<|k>Y?NgAYB0h}rc1@yw7*qUh45Z@6VRpvwt_cV z-1q27$&o{Xm6ZtYkP37WvHb$6&f~5&KIjbK3E)hYzkG;$d|I|ve2UFeu_|shYfHd@ z7r89LT6GhSidRP6%Od9MAOS<0$<^8De`XPMUVmM-sv#y+O~FiMG|GriSc$UeqO9w@ zA%Nk=a3bWiq0{krr#GLK$MW&8*nfu&CugSuI~TC{KJL z^k?=cVfr8NG7`4^WJrIpOT}vJ1yh)8(WGRKD?z8@6Rh}+0BHCi#KyE{l%d@OFOW70 zw0w?uWEZHE@QR&eTYQM_j@iR;qPbG>ijHH*Zqr0TT`)55g1}~uyK^os>f~L_yXch) z2TWE@Bae>n5U6eMx8Dm@whm!iwzT00B+dhWFktu{@N2EOv{CX9Y1#gnGnihpi+vrN zk#2Xel3JMoFQC#4E7k?{zu-y$(x+ysN->1YLKxh%r>ILdR4uxFx5O}VJDcT|(k-PT zA;)Rxz46lOr}K;gl~vAR2B+6iZaT&Sa^Qp|Y0PJeWYYGMkcCUv7_dTpq&seS=!!_; ztmA_qE}_B|-)^F6mjy0-3X1r{?eExbjYYhSSk!9?_sgsJPiOb^=oMzpw>K~{~^6-I+CSVXj31gr~g zutgdb7J0#!BpHao*wE5DI6iW1q`gRJgvVd=X0^4mWvUz0KJEHgQB3y4cdk?%3*MH{ zfm&YiA{dVG%*SkErnXT(yxuMdhj11r9PX6PNGD`_UOIg*ltVr6Z-J|BgK(M?a8h9| z?4N?cmyo99e6Xx+>PDkYyEEXuI2Ym0{J856a(0guUI34y*)SYMaX5XLkIB*OJ|`Mb zaseFnLN*QZFwqF-`81g1<|9#I?rgR#q+Zm)x2Oaj$(O>~;bHG?B5AcIQB@M1ik6X>UK(Chk>-U8$CBS`vQE;-+4?3!cQ z6ENrY4D^If6?6YVx zef~MYgYomti<~TWw>W?8kfh+GpWDeW|Kv<3v;wc6SHvQ;Ig(XyLe3=tA*QbcPFl5t z$(QIjn&zPt7@C9J47r1a7alY@J@Sf%Cy_bL0hGLBP9VlgtYsyd?48k#cV2-)%yb({ z-+4{*z%kFJG^sNDE=n*%OFUt2QD+_WLZ_GFdoVu*501Rl_|eimFA&f*FK9!cmN(WY zHJ&A9FVI<3b;~742;1HnLgPG(VL3;6f>^e*t3??3;+i|QNtG7fX8I9I#gpJphs1ZP zkn?`~y;v6h#~h9XenPJ#0dfXw;ot!uk&ASNhZvkfzSKhJRuE;ffL0W9wrZhQK!h;&&;*~FOy z|5L?NgZQ8^n+yn1J3kcjB4DO&HkCObA$j>f;w114+3#XvlIL7x`POmP0V$ z2lz*X;af+6(A!*+O_v?h3x|hqxiLoMQ$a9=fNtlp6mKqTwAM6gj=c@eDB3=ILJ{0& z==DTIclMU6ljZo-!FO_=?1OIkq=}LMc;T6X4O=rs-m5rw4kSR7xr1L)OJ&aoy<|R? zRqLeBmB%wS6R(W7`6wh!y4V|^KZd`Z8U-%(u&6utT@jgg>fYreI!tIUu+)o>UUuQY zQOD|(+7ttWhdBUvTDzB=P;T8R0Yiio@a9*_d+I1@Ad3lIrSr0RI#zZ(_GR7<8CAu8 zIKwT|Peh}*;ISOmZR==G)~TE{#a`29IZ7|PIH+a?&3MjV%%~_B)?787N~ zSVX+D^Go?;`Qs(zgj(SeQf=5J9XU$>2L~K zsfGy25!yf%&o~<8bk-&5A~4I!;ct+d*JTYnqHfL!kTj7VTeHm4dVvi&jo6j)VLWw~ zOghUqV&{iLX`PAg4tv^J_`?w<)lEGMFZdyAxwR3vWR>(lD)C7V?O0}#fl-1dhsi~Y zh3Sy=pG}S@^j|tNQfT1rY3zS7dX_6HmSsRwXRXHrm53Xq1j0EbCeXb*y3>&`!YS#@ zi%+B<1pEB+Oq?y_bQfy~=w5}8)gK^J$?i*QFd4#AUf~u& z2eEu`6yDrU_wW|V;eIq5nKvKiLzUf(Amst())F(QjM1|tjRn-V8Ln3qoX&m-(txNJ zNVkYGmM?^wN40Q}##sC_qCIexqHD0^(ppKvAGR+#iYJPLmnYLW-}9K@zj6oKgT;>n^=o(WHiQmJ?ZGgZOMhwj3hED>d`f0b#v zFv?lfeFVaszB<+#wVw%Zir0EJv@xRVC9uw#bB{oq&$?&E8#7RsimJ&VZypq}EK0xf z8j*`0MORgSahw`;Awt0EZPRw1`XOk>149pA9z@aS*c*g&Sx~l42>|HsB=bK~{? zhnM)IjsFxT__JL8@~21L|59%>UhjW-iBIPEv)dp#`2Fk6|9Z1oZyMvjS*tf*=l@H5 z#DyLp8X(WdF}WX0-GS=I?BYnW%VtDSWn9%76Ssvr;Z6g8vKQTr zvO!mkEc|*_rlg$qm~v=H+9@r@)08|QILIxVQUVD(59Koa4;1M!-Vun`d(xl^XPr1; zL|%2Y$f8leZzMcF3n{w?n1|OBfAXLJ5Q2=X(|2jN@A8X=Wd;k1!=LnyO_wBPo{Miu28@u?k$$$R1bH3L(`*aM? zRz!`DCqJE>emW`quHdmqS`%jFXuDa<;)^$)pxdGN+wZh5_jh$qja?=J11A!W*qKi% zk|39oVpymnim?18SLL$Nv~`6c`4RMkRVheH{J5YoGNh0-$1XnK|DTyZ07jf*x|!F|6XzryI?pN~7|7e(xZ#_8?E)thox(3Edk z%3|vX^Q2hLRS@{9Xg5hDo6{aEvo1~StRgk537Vp23(FUDiB*qWC%)h2i#Q#PwI`|;Wr(RfC`8U zZy=d5T_#C>0v-SiORO2fB*8R6Oxsr%!MLfiD>=lKkU^Z*sCeo#Efn2+#g1+0Pg$h9 zlesZiDhcFO5oTxK+uXSQ#tP}{MC zu1-z`Ak>B48~D@HvcZLkOV8xPpchV+t|!o)B?Nut@okpKuDQ_YD@T)b*n$D{rv?j{ zF9j30(EQ`DfvclWWCRUlepaBcLMJQa89Ob`JBzpH6Pyhl?x0h$K_h8plD025*{@uU zpG2TZJH?FBzEhyPbTVP1pvV@!^P;z%F2GM`(=-XLdPE;3!#7MB-jOK%UU6-I6bBf? z(&4^Y7upo$;w3>T3DQRR%Y1}!)OCEuB7&KwDKtQbyL99T@k!V%^TtDw#q@N z;mg?_&I-PN#*XswboVkD*$}TNM&{^rC>W*F8T#R<)8X+b+}X+-l8#6$^ST-RxKHN( zPjXN7llFk-?EkLSn``F&&t`M|b^qrjKIZ<fqUj>A*t-;w?sz7el+Z6pK}gJ83L+N;=O`een67=4 zinu%Df4M=mVit2XWU6F5F3)om3&58W)Rox=XafkgAAaHBM`>Vxm<0Ybn7E)b>@3)` zn8h58Pz`~KU5^@Px;qrM299!{g5kQY2$(&bq@}yZH@tw zHS+gbRxx|rMJ3TfndyWsjED5bfv->_b##^Db4Fe%Y2wJ}ieo+r0>)bb>9g0fem|Iy zBf0B$Z`s4~B!X!k2lP0;^T+eKPHFp+Rk1k3je|uvreV7XH={QW!f~bCAe>Ts4)RJ# zi=DZ?Z=uPp1gFiyUO5mIriFp2T4A2uq7=162Uv#;QvpJ|UQwxe$;w`(T6RY;{7IuF zVOEgP7zD|j9#XbYfdV!qkTS@{ttA(^M(mY?T*AaM$fZ`uH-FU{`KShkDoJ{MwZ5R8 z`=$reI(D2xNF_6sHPH)ce=i(=*&urR-L$!Q7@tH_H1tX?Y%hz!^tOh70U-SA_j;HL zUbW7WFLU&BLnHszy3Oln{wGuaXW7dpPYnQ@qyN_%8|$Y2->5ZT_5YXn%+de5gJ{;{ zS?r`n|B?NCVt7`1@D&TW!ZRK+;)=&#C1F`2?uwfe9HqzE5(27eC-APccrQxD)nM9H z=E%-?+-45o7}llG41Ew(YYFU13e>W)2(nNnXWT;qL*}KGsxQWqedM>~rB|)0Gp({m-;`BhG&iE6@p$x4F%57Ux&mCysdG|5k1EI3T!MS40^#HvtEYJK&lm9eD zpD6(R5%J$ml>e{s-(UZeJ^oBqJtg)VKXcaqMsv+N|50z$U)TSa_|VGChMDdjmFw}j zf5(=+GkRR{Iv-Ek`xm%n)DCDtd-lgEDa3f&NU#35)E{SOk(38Ux$+{n3;DKDVvRa)nI77iIGl-sRm@l`HhirT zk8#y`Asa3jmK_dPz_$#*3r6 zy7;0uc{qBWR@rTdc^js_8!BKX4+85Zcx$sW%;wq+qOJujfBs zC#M&k^ZlK@UqRLsPa@EDU@3oK zS?T0+%`2~MC<+VSaobZz8v(ZSF{PFbTIX?U`noZb~y&Y<0 z&*yD?+TZ^PAAAae&+N(Z>B+^9_~fz=U-x$i&aZ*b;kZ>YMZUW8#C6#PILbZh z@|FMPro#^~S^4ghx*eIdYhDpMKfS9)*h{?vh zGyh@vgM5^s)!ucn!Vdd&V)SD$-B_d#bTrH7wqZZwz;M-H1V@=67*Dv#gO(5F&_!-B+{-B?j7Q&;1{4g3r2>S`GxHQ#{8Me21Tx|v}iaCrzKZ$mrfHKtE|K+fK(RShOm0&&}o?Ptzuz&8IogW_Wod4?mwEt^`<+Zuk|K$P` zeWDEFr7HY-t&79s{r1Jq@fl{U@2NipgYnuiy-olLfrO9DdE)F*d;e&E_riPY9h{#Y zJ3H~|$NlsDq}`VHuiokT9`xwLuY}y*etXwDIy^qS@an+sCBDd1`6^Jc5_RKM_44*{ zYc^_yar#c8J32wh>ZR!NBsihuWz9}aZp(98!An@YewAvwv84eEHBw+|A{h%dQwl0# z8H=r@6f>5T1j|hWOFZ7IPe7e{@x3`Amon{hB^?wy>eKgGDdspKYoc1m5Mtjx|L4bpAG4_J-E&IkqY-bvY^#fL{UWXVp7fXv+9szONppiwmaMU9QD~0!$LIxz6 zQwS><&4y@1v4KOV6w)t8ks(NN{p#;BJ5}IJw#!;g&2Eez(U!a+0zf;sBb6Qd;tZ0! zMO7thX4Y3(EqfNLSYA?(t}dx+Hs{;LD~xX9hcOYp63@zOMjRr4GVvdDxsNq`e9V}Y ze>GVWXg3H(C0V%axZQ@MAfe0^(L$tBX?rx}tNAh~DW>#-|9&db3Dz8vv;Nxj=22|z zaS-$j8KEqkF+&<`$??Fk;<|1?Dyoo+VtrC!Id)wnoe4ZEj<3i48~PM0~#>gG!) zG)}VQWA-Z@9$J4z6e^C)rI_S26VLRj&)dgzp*5!sH8-=)-jJ`@aLJ|7%e(jCg3jJto{Igr4WGa!M1S}D21=)-TsxQrR=$;kC)bVD;2F2 zY*f06&$1-0w|n+cN++#M>J{W7INsG29iu$Ftd>^-?=$-83eOiGnnB{nlUmPKk8?q( z*SI-)WJXM!IbpHc~L-fDDEhq zN_@LlbIGKDPN9}E?}R+eKlc>1r&JvK-Os_4j#*qkOi{5&*LFegKvB3<^uPT7_P)J8jT~9@@5`sqfHQyxFyKcr z-eLAQHc5QOuWOu{-RpSywV|eeDy~jP1gjP!>sZ=Ue zNu??;7}61hcDW1wEtb_S9Vccr%H~(N+=M-^@$%Z*R}qv1GOy!F9#==s@Ol}JH;(p| zAIpyPD@YB{FCdeD&LX6QNnrWQU#4Z};sV=zp(d640rl|fbN8*!D)ki`3{(Lb0VrbF zHkwW+Uz(Kt`3p#Q2F6sxLA{}=cwi`g&(ptoU9#<}y+{5n6W9km21J(luIA9gvQ!!E zuzk@6KtUM!@9HEX+-~tb$YL6G>6R4WtEhG_paTwXim}L$OP5N)c~PDKS6EO^Lc8Ko zpk*B3+;TL#w3K>=k2N7J8-#QB2FAS%7w{3I=IXv9x7cQz)3h@CIV-C4oT?UJdR~35 zHX1xh&%MNk?LEti8jC!pHs7=2urMMD{W@14FXqJDPDXGHbewT0BWe?1C9?QFr5m~P zD=Scdmyz#{BX)}_gWt!H-#x`X1lYMiG7qN~v|7%DY?56J7N0o)&aFBxQYJ%R4e$q7 zO!4gAD#}O%fivm(DqGf;YXt$pkp@qMi^@zZy#t zmcmk!@FZ9|n2myQ7zOd&@Rg!@3k zrY*V-XG0!4@6yLDg89N0)smhDv}L=qNB;EpcIcmz-ww0(Y)xSFHxIevu#xv^pSyWPv(YFV&T#mbtYTrCmu@UR_aH3<-$Pl7jI&lgVWmqz*j*NA$;C#m&ea`QV-EBfC=sTgFj;QNo+ zz+%0Fhw$BRASh4#X%FwsJxV7PFx5>h<(unJw78^V1=*n|mRNc2i9+FZ`7sbEKq>U8 z_a{+6d29!Py64Gs|1JjE^7;Kc8KmU$;9)}fH|Xe1hBW>FiZ`Y3n+J&C52p8yKfMVL zF+%v`Q@c058a@(J5TKF;U%o#fQ&8;lnJvImn16{!Wy8Pb9Qw(d88>OSFxlYlcS zOef3e$DJ?Bn1Q#!Je%j*hdj3C9n9|GVst(Z79Whf`&)Rpkn$)d9%<8#``L43nsu0F z5Gm;Jy*Kqo0NJv;(8iVDN;HlV%}MRNsIkaAJp-1*-2I`D5<*xOltiC`C~V026|gZJ$YD;>Q*IH>$p|F4WU!O264 zjf}Db56MO9irJqEvu~5D1m(U&cw#A~G3sD>OL+`X8R=HJj+vnc^Dev%OyfE?|8!#X zw0&~0v)f*b(!Vvamt3>UuIBZCYI^I(X!9vjOY*bE*$~nhAQ*m(CK$$+J9MZj??0u+ zk1y*L*1+g(*CqC2^8+>dl@jr#@^bhL0^81wu+2!^EkeJ}S9J4buMPsPvj6+!$2 zFGQvq;wlzKDn`%BIs;$QEd&g7?(t=$g5r}aSBxef;exOR(;yyF!DZSmTW>v%hScTTNKcdfM*(ClAS|GV%y06WWwIg-?v#a zc}G8D+iX2aAU9&$Y$HiQCt}-d^Gmu}XF~h78gMROmzX2(PxjDHkZYJb?XzS|#hBBJ z?mI{LTB`_`Huoj(jB;N z6>;A6LUdTyD$e3T^%eIIidA*p?m;y~a6(sk$=_|sA^$*|F_Rk>cg?p{Qy1o`MYy5w zRtCOn#>6ydr8OnZ`IvDyz*&wIz-(Tf z7Q(D?NiC-OgSe7g7`P~9W^~~epQW#1${4!|Qy^ezR|`~ynz6~SEOM_@*dL(zHQEOW z^?7(ltcFMSy%TE@zduAQ2opiK=ClsWX)xp_M)VQp-yj$V(Y1AzXx@sqd;I?Bth8EQ zw8}{a=;C?SJ>3z|M?1p5Lk6^72Q;Q6R=SjRvrrec6^MU#wY0tz^^U@nATm|6&HZF{L zarO;1;w&$VimZ#-+Otjs%TtuhgM;-r(xU~QJBCx$)F>r`N|jHy@0?fb_)@CMM%@1y zI)J>Gk671&j2rL@g$j@6nS;e^rjYLUyZA{uU{1;JYMaNo$1m#E03|!AR5ijurvlgE zY|5@Ne!{gkzS3krc_iaLXOLg1cuVb6)@E@JJ(9pTmoOn}H7s{F`rfb$=6&~y@p;HXFSu64V0M-44URb32zF0-vG|Llj)&~)B_}s- z7wF4*c28aLOndybW`H@3`rh5#S%lwP25Bkk#Fq?$=cTLx&n&3XE*3ONw{JTmgoHFX zVmX}};XOr+gc;W{i^0ZCpcPqNS=&rEF?j3yALpWwGPH~?R_&&0A8VK&{?&JHv_=VN zR^L+q&F?Ol8rBJ>iFQmf@s25J#8Xin^O$gd)bmBtf?H8_W>N15gDuQOKF?ubU(q4_ z)*GtTE=yTh%qXZ~k{2{lNogi7(F?4r5}uSIu&@xf9#2AQTc8iu61S)Bc0VbJ4fGKHm##B`UzO?L*1pEhfU;LurSFvlV zs8*yl*R=Kd&kkAh{5;|_KmHRB1oUm=KQ`81Zf>OFKW;q7e|nOK!XDu;Yrz}e&>mlm z_e`(+_<(4j!aDKP_eSr$aes)fzwMkJRoMQ~o8t=GYrlT~Hm8HCUGsb@oDyy{^-n>H zM(y}bF!UuL?x#wDb!6dygK;zg@l^qtHD=1fgNB75Xio=IZ-ka&g4fb3H5-rDNLm%4 zrl!C_F>YQG)e4h<`^Au$lR>DwDV|o@A)C-#Uvjy_oxgSeyK{Wh-9Kv|cDg6+ z)9x;)A8(IOF-{arwj6zZvwzU;9vr{LxGF7HT)Xk6IK{PTkJ)gCtEd&&dw+P+#ZW3x zW~-iib8vjT*Tujqr@!tTbSVUl_5s1~AHD6;_CHmggQV`8Z#N+{1Q9^1i=L$#REi9e!Y(kO*1ux?)FjrvcD~yB60E>77(7D~? zqs}ql?cL7N-T^hjm^z^PSUIp62~G@bsg`>EzASH*hGq*7ePYf*g~p^R^}LDK3t|vY z>dlSS257Z7JN&rifRwJzfDPT#F|nXo?u-_|0>~DrIS+ksaDtLe#T7aRZxu1P)X4kv z8eOGAFX_o{fTKR>7moHGKfDI*R#4g-3WFqcKLfV&$CkF!f7c2rL^Z`e7#7zx;C}~3o^z! z^iLM5Fkn#I(#)CjtCDWy69vEfE#zhxWE5XSHHO%V>Fj6Lkg>U~(^7^U5;~3~G{tkz zF=jn{&2EF?ki00+mbrQO$$4g~w)DwKOhUHc!!@#0Knzq^{&pLYRJL>Dz^35JZ+4`d zIZY|BQ~JQdVPE6OFun|zjoBm;3I=x=3AtqNJyHv6z^5g3^dJ=w!E5SkRp6F68o5mw z$k7qTJw>2qZ5+MVXu^Ey`?$qU(tn?y!u7bkhB<4-VF@&dEG;O01+EGu7k@XZTD5N# zLR%zp&}Lvs0>x-OTlI}s^*Z~B7OooK#Mw??IuYnqB`Je&c-@xU<>c{N3rd?NWj64j zH19N7?juF}pYSGuHcN1RA5KcH`60Y#8yWGg1A2l>Mja954Vj$#Djj5t$qYBMhyv4W z2ZQAr=oe0WB_OA7Ud$)>ik_ET7^nBnc@spKUAgdC4KxVc-bdFCCrm83%z_0&YUtdl zp39+vNQ~HTxi&`av2c!rz;OafXEr6Z2|W&POJ!K)lYFUdS0FxzSmN5SpF;*|B1me~ zH^r$vDo-MW)jG3F+&wjLs`C-tTt1nGc*tE63$2m2!qE0BYU!I_P>OGomdvz&hQU}* z8i(xG3t}Rr39o}ye>O8NM+XKKX$Y0;KwkcAjN*oDoN^ryC@A$ zn>%PMnQNnfLLwku>ElXL{Xoj@lt zRh9;$>aVcdYk%CzkAnU$?kp&W(!|M$fZT=WmyM4SN_xUiQ$vvWPBqLqeXw~ zgP@5$i3q1KulNc_5NT9zrW`${hE>uPtP&YZ%b0;Bq~;3i(*NQ<0WYs~uu=k<_Xvyp zN{%{V9JE-p%IPR(*wz_Efah~X+}E-$bEaH&Ca`~Kv3%)8)Orz>^XvsH1(oDpUvwTw zy9fG<^gw8hp%;o7r48{g7{lK<_4s=X`LVZ@{k$s7bp`S{jw!rc>m=utzcM?uY z)HE{?8R*{_p{&LA@RE+k8kxHKB{Xz>RV>16znhek!M95bt&@=aDEEGR!we_X|-X{v_`U&GN&_n0?M51 z1#LWaK4bvmuqoEDS$ZC1H(Ubn(z>2z+p!2mIOGC;@ zp@YO+)sP-Yi&r6e(JmH~>P)c)r574jXO;J8ox#kpoUC0qbCw!~N%Kn2kl8H{=EALb zIveukIXjEiF}Oc;)TGB1E;Q?nt!jO}+FWP#_3h2|?Zy`JGAVq)Zxl-6@?YBbmOG`dP=!6G@7L*d_pjJK_g>2_fK;2->eFST!Lp%UB=InM$yX zTFQX&?HpdoMaygg`cKly3g#=H$(*?e-J^o!Z{~H_4O?&>=o9oZ>|0y1Ej);q+eHck3AP*YFIY;iW zdvdU|e*}{%kE-w5WatIsJS3x#z%UsW)nNc@EG&j=2v(eH(DF++kYO$)l9I46aP0$W z6Obwnr`0S}J6NJdRr%XaG6!rnq=Vtx~aq7X!ChQ7nSKLj&0_x0*=A0(2D zh<~9i4U<<=I^Rx=aR0oNuV3b^y=c)9RY8R{b-l?A<KIi}ST<*Bj{k45c3HT1Lr#qdK*ZVt1-QV7|+y94bQjFyP zZg)ESXkXII<)GG+ll`N&sHC2QB9r{NP~XlQq{HTaU0=^vSpL_|I{Z^tzZ&rC?a?0m zf)$~$q23ys@aHA`c?Ik)$<%DZpLHxv;F?&duD<>VDOe8jzfy@WwYD@JZE31s>&vv| z9;DTaXzbHj-8}e#8X>;NYJGa|UX&usIN>Yw{OkAL>~{|0{@dSmY^?*J^Y|9`o@ zk(&Qon~mrB{}hk4=Yzvu`z@IILs+&B64P~=#P^8EFj`u?tEni8e8D^%VM*( z(P*-_?|#R<*&T8P#7=OdlfJ%wwY|By4O+y`?)IxU+k3At*CC7}eLeIdjME57zxW(l zI2hpT>mWXb#!=Gd20Of5!y6(+l%V%0v2*&j>Tjp(_4;q`ieCgzz}2li?oVi!l^@V( z>_LjYPFGLd(Qe&J}jaB}l*7vy~mKXJcpjgiCeNt4V zE-Id&Cn+q_78OkfFeyrNBY$zO=>Nh!wvG~f_FQBTkD~yeQx{oexNHfN2vjL^A}&CM zAe5vq&CdmeI1wdfZ~~``Xfso={Dl)Uxq*t&I2|@3nR2+P{$;phM8~F+9gdWFR5Ixz=Ir$?MG8a) ziNqvGVdqVE|EPUdkw3Pel7ScrOPWtA(sF3TaV|ihW!!lYb7(Ajfr~M{ufn`cql47E z;w{lJwTm|VF)p-^I-%s?EusT{gCOm1*$^$DnS0Yf`to|>CiZ#F3tM;+-ulxH5$d1O z%n4RMr(cx7ro}71IDD9|QA)FqP5l**P+v zpnQ?;AwH@-_N-2``oibN%WZc4xp+AZy?&3|kQWDoZS#LrPH67K$Bp{7x4G>Pw(FbQ zTd%fzm&Gp^71m5cY5dr%HMU;W8nx!;CPDygj=bn2euI>n{8QJVCxse~IzUeM4yaTh zpYGkr-np&*)#v?_n+-sRdRKhSS?^L#M5*~nYw$f+@x{?hfT-K|?(l{EQ9piXDx zk`C{Qi!B!}w(gCLt%o3E>p{raT7--!O2YVUzbtrsIAG5To0`YA2gT{>%4ARcG};*Wck)Cipe zz{`C}+WnfLp;drV=)WHzg;N1W;QgLR;g}R4=@E;#mXS!5)^`A^;^H}ft8Gd~?zO;X2}TX>d8|Vv9bVh{ zbd!T#LhLgodwB~>L~?1-6Vt^qg)jrw1F6j>3wiPGA><}=kxi<|V{A;BXF>VTU-;T! zUf4=ZttUb6<1p(aDajyjrHZO@2@;&Q`}wtg5wpcq5YxN+OxPIvQLJybm8DIMN)q&< zGSGLrQ790hmdgFR=Q3)YWESX!ER+cOxa+mdsqgFt(Ezi!gozz!sX-}tp_Wp#OwL51WJ-Mj0uLvO3_6iMbLhE|F`PNYJAozXO z^S(dv;%V3$`lvKqdVMP2H#<&%d3AEckH2Q+iw#a@5vU3uf#AOLkQ zLNDvMw^>M0^Zz7M?gu?{?LY7@8m4b*|Fu!yOx^$4c=_!A@kt)BDB(zR;|lyGm#STV zIIOTv@7fD`2%&!xYjcjB?{Zjg|So!>^-KeMwY^c2Y7*_m|$3zE0&0vgQ)>Lok7PK818mR%qr%s~Fm&YVzcAb)3P7Ni@{ z4 zS6=G4&YEvFPgrVZ;6a3-^Q_$-0Xs!-kWO5S6JzC$=Cz2dY4Lu~4QmG!Og*T`U zr@zeNaO%tB&J+a|MdY*e*{$JvC;3r8%I|VHxutP=#b$E_z_3u4c|N(bfZuqncsmqc zc!&`rJNxgZ%l`Xi{n`J=lRPBJdzU?RAp3e3eO6Q; zEh0yFBR{?l`|5V+>{7U$6mHcu-{;ZL_a_gq&@PCes>-Z54$1v?bd5^{CI~i(iTpkt z_jxz!m*9`ufr}>LI1)!h>D{-2mLcgy?Sw9lCAnxF8^PK@wR86Z=GQ3hhqJh)!`MG* z<28KHaHe{C=WOrz{aLxjONgsxndch?SxjWNeHZKBgYn$Y(2mr1+5L*ZW{(SF3kO2m7Ik-kM19o7wh{mh7>>&!y*g>&S?nCCLT`ldQ45mYLqgC>vBMQw)Kb%F=v)* zvEq2t8wQpiOFbuc53?u(!K4!oJ2Sdy=&tB?;&r1L_sD@J*7Iuo4Fn;T^{Zjrp&LuJ~pifT3CgjDR1hanTzj_HO7D@Z$1sjFgTFlkoh z(KZ@Zq>$nDy$Q_6NL8b=DD7Uxz1Od0RxW9NsIOQV=HWvJ$hm8jfbCqH?or2_)kwD9 zu?o2>a16X0YR>(!bWhjQQiRhy-el25dH0{RIK8NXjM-?vEQ8! zoW{qH5``B*+|}}6b+aaWWQmHLV1>ZgRgMa~T_A3u$nYlrm!-|zlPhpn3v&UvXZ7NO z8ihT)>>{FC3~C{1a1aYN^iB*#0UtX~_1Xgp11fZ^d%<$FndzKKDEC@p%Bqvw{ z6J3KGRD%38@uqlR2BjhVshGCPm{2sVsTkM={vZ232#~tBbSyK#*weRYalvIYZ-DV# z{7*3`wUB!zm%;)oj=WF7Xf|Tw+2|7TK&80BjYToYO8^4q1d_EzU>Hjg1jg%K`~6~q zBeF2;=uU)9<`A?Syag78BXV0D3DMoti(Jlt zUn{x;a{1>h!mt(Eb%QRE)&BTYU_*?#62|mF2W>zp3h-Mg;ug9RL9Uew+itoc*XSt> zF)G?LoLyZrq((yNL&vQ^1i{-%85p@g;=X)}w53o?4Kxj5;w4FBbGS*;x7D>S0E}A< z-PIo#V=h-J#VSU(wYPk508gJ25n?S;eHX#DLM zrWg>LVd|R_a>&MhvLk_eEI5c55Y5J@J=prv|J6E80TAw8s^UwJ8IXQ)83N6^zIuPe z7iHIq6bCMF9(;0Xg8<{|AI_%Q1`p{o^Qa-+XnBfmjc3_0pvHaI)a~4Fs`i*y;NC)Z>jD@O2>=a(Pv}R6 zJolNP*NR$3SR6%8GZXI4477|wu_!nJW*2=T#l$3J(ca`mpyLNUiFe#RQJn547r{=7 zXVvpql8q*fL@`K@-uO~`hfy83U1zIsQJwy zdXHvM_G2&{(r$viGpkEs6xIOyHVCd}Q*vKU!Qv8r&qOrH1Zj30ND-`2?l!ePfBrD0 zApsn<@KOMOokSnCH}oFRr5ue9<1b%6jF*wM%6hHxsBbwzFu{v82-N@tzG@HudOb&Y zN#;sLqaRa_p@23#UWBDqqnIG?J$@kTBU~= zw_0@@54KDC?zuczQBpWg#k65nmM*1@4#_Uix&0IE`=yVO0NFK=n^Z~?4e`?Nd9%nT z1Yom)=9xSbn(`?#48^Ktj(7hWm_M%9nsE9RB734 zcBl(jB#8|JphUVsxt$jolgpo!qve(X^{obxj|4Fh)VEa3qs5iRLvRlzGWRRE8>{W~ zc?{)6DZXKnUE;Wna-26VoW90zk3i%E>P1&tYrvt4nHqr{!vBvn0ZBhktOyty7#*Y` zzEb-e%(b_nD+n0+(u1`VQEe&--=#Hz0iM<+IHe6r3PG)o?%W!QRw*$|1tBea8}&VF zAnJKK4Wz&~QeaUJr?^<$`6+w~M?nQV6#koeR&4I)yUa5FEw=R^TR2^~Ri@*p3jcV) z6!9y$CrC{6|FS}pEy-oF9#&-(Ukx7i3`Fc9ma@ z&?^T*mqO)I!ph`&>QT+bUD3MVg6f+MTIyUS!9h*t}ZDvw+j;o?3o0S;U{c zGiD4776f_n+MD)oJ+%0PmFcTiN^WN0_sOAI%d*|S|1zqBd+1JDkoNGFv2v-Q=BBN3 zY#nY*=JnD&`nwidk(Q=ozvXa;t6j3CZ&^{B#?E8H)-p2kS|g zu@T_tfBD{>%AUW=#k1rV{rq5qQlo%*>7QUgQImoz=o?i)k zbQhVhuEX$Sg>}2hTen+b-t_9G!baYwDEM9UCcegddw7Z@%AZk!0Lz_1NY*Kw4|Mk5 zzVDni=*Rxi8P5t5DTRNNnE`XtVK53}YN`Brda!@Ef7adUEOC$WgkSheI%(wYdoU!* zKg%AyahG3E-66b9?+&oe(lna(pwIplD(fJ32{i^W011@7%3zYd+I+dizk~y5Deelu zchwDo!)5ZwNUN|C!r~>$Wmce1xMhT=1|NfAczGB5%R#1)jd*DtA)s@LD{hawUF80w z2m)vZYVj%@kB4{IAm|8oneoa2G3(9hWkBIhV}HbVL_l*AL(hH!RIIM9H}!2v18Zua z$~mW7I$n^dvQ*sl#@xOwW~%eNJ(@SVGKD{&sUmh!VP=VA{Nmb;H(i^~#u|t2an41=`_V|#vZ z))YA z>Cc2Q#a7<7X#i`Fug0HO;h3&^vz4J_fcRmpU)rS(R4IJql57sn~i(k?CM(M zQxP575)0X`x5=yW`oJZmg$DI zx1*72{(Y(|d0raHptTHef-am3602+*SkA&>?>ZRvMTv-EUo>pifa~Iw$7>3Baf$XG z!y%o84?fLC3b?Z$&g^Sa8`Ac(0(r);3X4Vr|Ezwz z#b5d^kPa*(cQO>Sv0gFlc0ii8cRHXvcr+*myo*>WX%MX~QfHtGy&j{ixkbr&S}8?$ zu&5EoY{O)CSzhBsJ_jU+UsV0FpG|Yz#t~7^`6D6&cWf;SD(DLyygRT$`K-aCUBV#d z`+d1&&Y&M+ofdLVvdwXl?g!Ci=-s{ZhZ9N4+|Np0ac1eqph_FKv&g&hMbzO`&hzT^ z1^m?$c1QSoh+G4I^e@a(=G!r!7~5tlV^t8!s?}>~o{sE2-fF%hjvrISdRR=zeYEJ{ za}aucCKFM@z%QwyLo5o7PRdcyD}H;^QJ{|nXijQPZ1WqRd+pcn-=<2$SOT3pvUCab ziHWgpWIebHl>~z)d#sb7&t*cj&Zxx@h1gmU4bmSe1Fn2>5&nVYnKwkjxWg)xYHf}d`>VDB@aCW_B7yPju0Gwm`b53pCUDq18UfTIODI;0mR*6k2MkU? zq9jgWn(-!=1$)zA65FXjW_D5#kv?FYwBXrBL&~CvInndmYb+eFpU4D5YnH$@rkn>C z$bQcA)gFd1uTn5`IX9|A&=;0$qIZCU_@QB5F$#WYC4sYPkKU|mNR=G$s-Tn3dXmmR ztdeU2|BhEeT)?WpU~cmu^*Ich!!{79pv{MH!XXGcuwf*U?-D&#FTubV3fnl=7$$BX zoJiB+#I^=21V0eYU>%YQMY0%@vN%KPw9nYPW8g`Bp9Ow6~waYM!B@x7*;@q3}N3HxyBcDTku~o{i{1?c)JDp#;XUE;s_Uq&0 zvsOL#`e0}GmxKMzS&Of&VSh3WFa277ZR$mnpn5s3z)ShG7rZq>uhx%Znm^8skcF-nb}>546Fde`=~vgR!2z zo}|OFF24j}jpXLRxGX=9yuRP%JD05i$*a2E6Uo!U+==xuYMHqAyiJ$pOZk5k{`w+X z&DUzstDWA1I#AEWyRu49ogat>2+baOn`3rO%h zs|B5x0<7jnHwvwU?A({!)iMJ^W3p%b#L+X^AJc=V5 zGi5qr)xm-SLJjJ(YQS^iCP?gEvJXP)fhzOS+CK^(ptm2&g%8o{>B;`?o3}W*+ebUE z589Bl_Mx$sH;Sy8IrsKEn02i$BAofU5n!GiW%|`cFsqcUJ zi}<W`rwl`B;Ll*O1AJih zx&s(IDZ#@Q54~65QPOO}5FjJ(hcpa&O9zQ7Z>t_&%U;OnQkXZ1tNhLt8T%@{qIEqw zwy~~6!eTt9QelOc=;m506wP!v#vC*=i>4%v2%LUB!92!XxW|ApL5*{Wh=BY|u1H7v3{BTi@A0Cd-FA_hepN-K``qenVnM%Mze%Pc+rbtPE_am=IKTPqn zL8~kZ9B9n+JIIZt@@K(0V@?9{gW$<_KKX8QV=1QQS7eTSiZH30#ZeGNXec1_71N#h zUjko(#U~K|;3E_JzSUSFzsbj|DtVTtQSouv8;4^$ixpSw;@^RmJwif(tu)a1w@#B( z&~wiN?0Xf~p!>3%X9h7Bo#~TUlnShaSGA$jw312_Y>$n?**K=)0(`apPD^3cfAQ@4 z;+%{h%IIP)u5b+l=HDNGKucyobRsDyg#eNatNy>B{Q}MCMat-g+}YKHOYT3Y`w2=( zfIL%Q)tRt;%KK@pApXrS%1Sq!MRxvsUXelYX*Ll>wBJ|+XQeVVU!pS&qcgkjPrGkV z-ggh%hpm;8DO|Iw$*eo_N3uZKqFt?Qo@4*qPOT0>p=!Reh&ysugBkU<3A+Aje9u9i zlBNuTr7nI)e4eycfx4PM95|H9zvC^CK`#P_BVPKiq&y>=n1!!?1G#v#)n zm22!|=%Wb6*NnV_#2GtnTcRAbZ3U7gJ#)OPl!nnlo1rP}Q+KJ>BUf%RhA z1MTLck};2eZJ+KQ9Pj>O^EDw+s_&XiNw*mk^TIYLxb%&^tk+p{v(7f)-{hXWS^y41 zkPYH22>dI1wE%|S4`!oG81$9>C}F^#W*WvjSk$v1;5YW;W>S7DUH)t~y$rJe&_~v& zLlZXP&sN%Vo+3pV4HhJ2F$8Zq>SBN$w$fD7 z`TOtF`Fi2h7a7;*W{mpKKaq1|Zcc1j_a+?1-W9AYn`T~)t`0~!2~N1@MwFw9D~)1a z%o%Qp)fG3B*=JzT+|rA6FiBTVH4fA3r&vsZ9r?G_D82*UF3imcD7HJ6_lK5PI<3JH zthG=u?pPWX(BXV)*A7eFf6Z_&7$7d|ptd$-o4n{FiLH1y6(lVQ5;}9oZoM&Y#ZmjW zq!lI`%2;3WD_WVnvDBpbq*zvuhy8@NM>uDZM3-9GY2|T&Zv5C}&E^Jc zZq$(x$Ub!U+MV6g{gbo(}3LoHi4H=uX>5XWeAP=pVUAID546*)H`(^1&pU6``#!)B$_I#c=f|tm^4K zY0T*b@~69TPs%8?b=tiSqZkh=)pCqfjc1$aSk<}jv!drf9T&9$2}TJ{ATWWn)M&n} z)oTs-Z*@g>Sr$z`euxd2YghRMXq2H!89IazSCNmRvm&8HNReD<5%TMQh8`hXJ4X_g zS-NY5P>$7+sYi_HvaXQ)BD&d37>ZC!u72);@jFw6Nuu zi!82g)Z*erUzFY=WOHqXd?Rf^QO`ns0h$y=sr`2}G#`dnYg7NqN55p(-H-kq-N)c~ z{*L=M1;WUKvIbJ;wcpRrE0)s1_QwJF-iAzg-~-ql`(mew$26I88#kqM-Fg8J(u2V* zyelsl*B+{XX^qi$|F9`yhLZ4zwtqL_N9`+7MoySAWe$oBQ%q^cC?EiBAh-!lJoV*D zA=!DSeRj5g^tP23UK#V~b%Xl^L4GPKz>~ba_M4se2WKs9FcU5b1D6aNudIu;wN{jD zrFA1!xvYkxUDkvKU;}S9jK|@FM!?N?Ka}+LeJHQ274ZCJPKkn9r1qh_rb@^z2=px+ z!KG@zMlEPlx=Bedh{@h#IK(02faQ%wjPva0pIw_Uvda<%yL}N#+ zL=Bn%gVv=h7DX5+;~?_4ICU?@)`)!P>mM^467Wi?sWFq^`UDjBuGk#c2VX@ie9TU?9KS2p(WH2K{k%nRxzz%|DPjr5- z{pg)WwaC#LlSj;LShWpp)2B^$;!VBLAN`pA zEw@VN|EOH7R?4f0o-PRF<-e9%rEvTQS~UCt$oZpDhNMbuwfxuRMpqs4yKu(61)|By&gmhMLgJ`|?=%W#^w&HSlO=ToEW{l6 zBE%bE?xNJN43?Tqe&uVgbGCE#zN0a~MJ!N*F&LOYv;j7bm^7}UxRAzKbl!4b?&9Vz z3pk8hJj&Rko=Towx+n3KOD#$){}on)mh6#>$v=d&IqU`N0WaTzQdmf}A$x5wkq<;o zGx)+K-)p03&3;Y{4tAtWyfQhGG7__yk}7(Bz8y_qu54diAU4jc-HXfKsQI$~Dz6oH z&1%ss&amc)DN^xFwMYXhbV~J6l`M3~dn`La#Ji_GPWg}i6oaUb!v1V1r#&tMzP=Cu z16UEQWKG;crY&HN9W;p@q?iHbxWA;8bW|5pmcY9qygUFD;Qz*eAx#4reiW6YKR~JF z#5GrI3d{CvamVI>5{Kzf6d@<`+9)V7Bqy~_llr{QtQlMR=v8J&lsO7&kE<2# zBAHTA=Vbq@q@rjNJWEB{O9!Io&lQJ=CpMfs5yw7L40@J={^O(|AhvHI{s7qf$UT^S zzRdF)}0w{D???kXsu&AAX`a=$o9n9NT7>tqwR_=(TW| zPb1qhY@0hGQ%G*5#zi!af6VJ`u}4Vm*E?pIzW1LiD@z8!|OjrgwXX`+1zE-mD*lVwUc_-`hX#9>2E;+ZzR+Fa`=* zzl>+2px;^twyQGdmGy#dT*nxEIss7mmC{Y`${z!sSdyzOu42)-HP0!*7>wgFXh>9( zScRVo+ttH^3q_GrI38HPJyH% z-g%ce_Yg_EL&V}l$|eUcZRABN?}?Qtx}jsO@rtJzmGExdHUz3g^X2<$S(x}At?d*O zMHIiG{UjdLt9u+G0*(qTD^9b;1qa55nqPihF^B|xmZVLVi(I1vqWRY>nEL(VUvh)s z@$Uax_rK9C_hY#HG@b?if7f5WY^Lu2uRq`aev*f~vBY7I!*Hm4OHxXOwPlc?oJbEN z`D=39&+$kx!5~2M&lWq!&CPeGsZkOi``oe3yzH^8Xj{c!(`iO-IL=*EnqvhM&l0cnOn zh(<#n93`Z^HMZ~oSn3RIjj@e)F-T?jYu!=mESv;A{MZQxG1(Vsn3dx0y~@Dp$a6gdCj2fJhX)H9Z80}6}nUx3#V2!QJ8L`wQ!Wt z6U^im7Lig>6jL4^F;kbkM&Ym8HzuZ-Qw!J}-9O?;UsF@9z~a%IOw4Gr#4T`@m&; zaX1YqdP&K06olSyl(ADf)o{^SR4t6po?8Y+i3 zgV`}%1c&G9`XetG%3Qd>%I|%j`y)|x=}@FsAm&}^a|%NrcJ+c5%%NZ%ddu|hAN@Ow z$`m53R)M zuk_GvrjoxXGl@Jbn@OSd9s!cVX_RM1Dh7qH^7XEL-19S2QC9JRG`oWtNV;}A`>w`I z9C>38W|sR~Ke!5jGq^wU7H53B4*X%?{?-d&<-tx~NNfwv(-pE7U(i|5K!I`aWfCEF+0QLjuf43*F-c;hsOrJ1nsl!%N=DUYG}0Fj zKsYa^{PF;!k{;1u^3o(9h5alTTDc0Vhv Date: Thu, 10 Aug 2017 11:29:10 -0600 Subject: [PATCH 040/129] Get BeagleBone make and model info. --- myDevices/system/hardware.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index e9c4809..296de34 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -44,7 +44,7 @@ def __init__(self): try: with open('/proc/cpuinfo','r') as f: for line in f: - splitLine = line.split(':') + splitLine = line.split(':') if len(splitLine) < 2: continue key = splitLine[0].strip() @@ -85,8 +85,21 @@ def __init__(self): self.manufacturer = 'Qisda' if self.Revision in ('0006', '0007', '000d'): self.manufacturer = 'Egoman' - if self.Revision == "0000" and "Rockchip" in CPU_HARDWARE: - self.manufacturer = 'ASUS' + if self.Revision == '0000': + if 'Rockchip' in CPU_HARDWARE: + self.manufacturer = 'ASUS' + else: + try: + with open('/proc/device-tree/model', 'r') as model_file: + for line in model_file: + if 'BeagleBone' in line: + index = line.index('BeagleBone') + self.manufacturer = line[:index - 1].strip() + self.model = line[index:].strip() + break + except: + exception ("Error reading model") + def getManufacturer(self): """Return manufacturer name as string""" From ac639b09d982e41e01ce5afa0c26e6068bca1604 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 10 Aug 2017 15:16:17 -0600 Subject: [PATCH 041/129] Strip unprintable characters from make/model strings. --- myDevices/system/hardware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index 296de34..5e8295a 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -94,8 +94,8 @@ def __init__(self): for line in model_file: if 'BeagleBone' in line: index = line.index('BeagleBone') - self.manufacturer = line[:index - 1].strip() - self.model = line[index:].strip() + self.manufacturer = line[:index - 1].strip(' \n\t\0') + self.model = line[index:].strip(' \n\t\0') break except: exception ("Error reading model") From 4d7f6e4f1c5d473bc4761b6a4500953cdafd1ca9 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 10 Aug 2017 16:45:03 -0600 Subject: [PATCH 042/129] Add support for BeagleBone GPIO. --- myDevices/devices/digital/gpio.py | 17 +++++++++++------ myDevices/test/gpio_test.py | 26 ++++++++++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index ee4f4ab..fa9c0fe 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -75,6 +75,8 @@ def __init__(self): try: with open('/dev/gpiomem', 'rb') as gpiomem: self.gpio_map = mmap.mmap(gpiomem.fileno(), BLOCK_SIZE, prot=mmap.PROT_READ) + except FileNotFoundError: + pass except OSError as err: error(err) NativeGPIO.instance = self @@ -190,8 +192,8 @@ def __checkFilesystemFunction__(self, channel): if not valRet: return mode = 'w+' - if gpio_library and os.geteuid() != 0: - #On ASUS device open the file in read mode from non-root process + if (gpio_library or 'BeagleBone' in Hardware().getModel()) and os.geteuid() != 0: + #On devices with root permissions on gpio files open the file in read mode from non-root process mode = 'r' for i in range(10): try: @@ -210,8 +212,8 @@ def __checkFilesystemValue__(self, channel): if not valRet: return mode = 'w+' - if gpio_library and os.geteuid() != 0: - #On ASUS device open the file in read mode from non-root process + if (gpio_library or 'BeagleBone' in Hardware().getModel()) and os.geteuid() != 0: + #On devices with root permissions on gpio files open the file in read mode from non-root process mode = 'r' for i in range(10): try: @@ -275,7 +277,7 @@ def __getFunction__(self, channel): if os.geteuid() == 0: value = gpio_library.gpio_function(channel) else: - value, error = executeCommand('sudo python3 -m myDevices.devices.readvalue -c {}'.format(channel)) + value, err = executeCommand('sudo python3 -m myDevices.devices.readvalue -c {}'.format(channel)) return int(value.splitlines()[0]) # If this is not a GPIO function return it, otherwise check the function file to see # if it is an IN or OUT pin since the ASUS library doesn't provide that info. @@ -365,8 +367,11 @@ def getFunctionString(self, channel): return function_string def setPinMapping(self): - if Hardware().getModel() == 'Tinker Board': + model = Hardware().getModel() + if model == 'Tinker Board': self.MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] + elif 'BeagleBone' in model: + self.MAPPING = ["GND", "GND", "V33", "V33", "V50", "V50", "V50", "V50", "PWR", "RST", 30, 60, 31, 50, 48, 51, 5, 4, "I2C2_SCL", "I2C2_SDA", 3, 2, 49, 15, 117, 14, 115, "SPI1_CS0", "SPI1_D0", 112, "SPI1_CLK", "VDD_ADC", "AIN4", "GNDA_ADC", "AIN6", "AIN5", "AIN2", "AIN3", "AIN0", "AIN1", 20, 7, "GND", "GND", "GND", "GND", "GND", "GND", "MMC1_DAT6", "MMC1_DAT7", "MMC1_DAT2", "MMC1_DAT3", 66, 67, 69, 68, 45, 44, 23, 26, 47, 46, 27, 65, 22, "MMC1_CMD", "MMC1_CLK", "MMC1_DAT5", "MMC1_DAT4", "MMC1_DAT1", "MMC1_DAT0", 61, "LCD_VSYNC", "LCD_PCLK", "LCD_HSYNC", "LCD_ACBIAS", "LCD_DATA14", "LCD_DATA15", "LCD_DATA13", "LCD_DATA11", "LCD_DATA12", "LCD_DATA10", "LCD_DATA8", "LCD_DATA9", "LCD_DATA6", "LCD_DATA7", "LCD_DATA4", "LCD_DATA5", "LCD_DATA2", "LCD_DATA3", "LCD_DATA0", "LCD_DATA1"] else: if BOARD_REVISION == 1: self.MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] diff --git a/myDevices/test/gpio_test.py b/myDevices/test/gpio_test.py index e7c0991..26f5949 100644 --- a/myDevices/test/gpio_test.py +++ b/myDevices/test/gpio_test.py @@ -3,21 +3,35 @@ from myDevices.devices.digital.gpio import NativeGPIO class GpioTest(unittest.TestCase): + def setUp(self): + self.gpio = NativeGPIO() + + def tearDown(self): + self.gpio = None + NativeGPIO.instance = None + def testGPIO(self): - gpio = NativeGPIO() - pins = [pin for pin in gpio.MAPPING if type(pin) is int] - for pin in pins: + for pin in self.gpio.pins: info('Testing pin {}'.format(pin)) - function = gpio.setFunctionString(pin, "OUT") + function = self.gpio.setFunctionString(pin, "OUT") if function == "UNKNOWN": info('Pin {} function UNKNOWN, skipping'.format(pin)) continue self.assertEqual("OUT", function) - value = gpio.digitalWrite(pin, 1) + value = self.gpio.digitalWrite(pin, 1) self.assertEqual(value, 1) - value = gpio.digitalWrite(pin, 0) + value = self.gpio.digitalWrite(pin, 0) self.assertEqual(value, 0) + def testPinStatus(self): + pin_status = self.gpio.wildcard() + # print(pin_status) + self.assertEqual(set(self.gpio.pins), set(pin_status.keys())) + for pin in pin_status.values(): + self.assertCountEqual(pin.keys(), ('function', 'value')) + self.assertGreaterEqual(pin['value'], 0) + self.assertLessEqual(pin['value'], 1) + if __name__ == '__main__': setInfo() From f8b104482e1cfc8f4a304a72880551566cc9edf3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 10 Aug 2017 17:53:28 -0600 Subject: [PATCH 043/129] Use Singleton class instead of maintaining instance variable. --- myDevices/devices/digital/gpio.py | 57 ++++++++++++++--------------- myDevices/devices/digital/helper.py | 2 +- myDevices/test/gpio_test.py | 4 -- myDevices/test/sensors_test.py | 27 +++++++------- 4 files changed, 41 insertions(+), 49 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index fa9c0fe..b17fdcb 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -17,6 +17,7 @@ from time import sleep from myDevices.utils.types import M_JSON from myDevices.utils.logger import debug, info, error, exception +from myDevices.utils.singleton import Singleton from myDevices.devices.digital import GPIOPort from myDevices.decorators.rest import request, response from myDevices.system.hardware import BOARD_REVISION, Hardware @@ -32,7 +33,7 @@ BLOCK_SIZE = (4*1024) -class NativeGPIO(GPIOPort): +class NativeGPIO(Singleton, GPIOPort): IN = 0 OUT = 1 @@ -50,36 +51,32 @@ class NativeGPIO(GPIOPort): MAPPING = [] - instance = None - def __init__(self): - if not NativeGPIO.instance: - self.setPinMapping() - self.pins = [pin for pin in self.MAPPING if type(pin) is int] - GPIOPort.__init__(self, max(self.pins) + 1) - self.post_value = True - self.post_function = True - self.gpio_setup = [] - self.gpio_reset = [] - self.gpio_map = None - self.pinFunctionSet = set() - self.valueFile = {pin:None for pin in self.pins} - self.functionFile = {pin:None for pin in self.pins} - for pin in self.pins: - # Export the pins here to prevent a delay when accessing the values for the - # first time while waiting for the file group to be set - self.__checkFilesystemExport__(pin) - if gpio_library: - gpio_library.setmode(gpio_library.ASUS) - else: - try: - with open('/dev/gpiomem', 'rb') as gpiomem: - self.gpio_map = mmap.mmap(gpiomem.fileno(), BLOCK_SIZE, prot=mmap.PROT_READ) - except FileNotFoundError: - pass - except OSError as err: - error(err) - NativeGPIO.instance = self + self.setPinMapping() + self.pins = [pin for pin in self.MAPPING if type(pin) is int] + GPIOPort.__init__(self, max(self.pins) + 1) + self.post_value = True + self.post_function = True + self.gpio_setup = [] + self.gpio_reset = [] + self.gpio_map = None + self.pinFunctionSet = set() + self.valueFile = {pin:None for pin in self.pins} + self.functionFile = {pin:None for pin in self.pins} + for pin in self.pins: + # Export the pins here to prevent a delay when accessing the values for the + # first time while waiting for the file group to be set + self.__checkFilesystemExport__(pin) + if gpio_library: + gpio_library.setmode(gpio_library.ASUS) + else: + try: + with open('/dev/gpiomem', 'rb') as gpiomem: + self.gpio_map = mmap.mmap(gpiomem.fileno(), BLOCK_SIZE, prot=mmap.PROT_READ) + except FileNotFoundError: + pass + except OSError as err: + error(err) def __del__(self): if self.gpio_map: diff --git a/myDevices/devices/digital/helper.py b/myDevices/devices/digital/helper.py index f45bcc1..4c5460a 100644 --- a/myDevices/devices/digital/helper.py +++ b/myDevices/devices/digital/helper.py @@ -42,7 +42,7 @@ def setGPIOInstance(self): if self.gpioname != "GPIO": self.gpio = instance.deviceInstance(self.gpioname) else: - self.gpio = GPIO.instance + self.gpio = GPIO() if self.gpio: self.gpio.setFunction(self.channel, GPIO.IN) diff --git a/myDevices/test/gpio_test.py b/myDevices/test/gpio_test.py index 26f5949..efb0d4e 100644 --- a/myDevices/test/gpio_test.py +++ b/myDevices/test/gpio_test.py @@ -6,10 +6,6 @@ class GpioTest(unittest.TestCase): def setUp(self): self.gpio = NativeGPIO() - def tearDown(self): - self.gpio = None - NativeGPIO.instance = None - def testGPIO(self): for pin in self.gpio.pins: info('Testing pin {}'.format(pin)) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index f66c3bb..4667fc8 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -22,7 +22,6 @@ def setUpClass(cls): def tearDownClass(cls): cls.client.StopMonitoring() del cls.client - del GPIO.instance def testBusInfo(self): bus = SensorsClientTest.client.BusInfo() @@ -38,17 +37,17 @@ def testBusInfo(self): self.assertTrue(bus) def testSetFunction(self): - self.setChannelFunction(GPIO.instance.pins[7], 'IN') - self.setChannelFunction(GPIO.instance.pins[7], 'OUT') + self.setChannelFunction(GPIO().pins[7], 'IN') + self.setChannelFunction(GPIO().pins[7], 'OUT') def testSetValue(self): - self.setChannelFunction(GPIO.instance.pins[7], 'OUT') - self.setChannelValue(GPIO.instance.pins[7], 1) - self.setChannelValue(GPIO.instance.pins[7], 0) + self.setChannelFunction(GPIO().pins[7], 'OUT') + self.setChannelValue(GPIO().pins[7], 1) + self.setChannelValue(GPIO().pins[7], 0) def testSensors(self): #Test adding a sensor - channel = GPIO.instance.pins[8] + channel = GPIO().pins[8] testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} compareKeys = ('args', 'description', 'device') retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) @@ -58,7 +57,7 @@ def testSensors(self): self.assertEqual(testSensor[key], retrievedSensor[key]) #Test updating a sensor editedSensor = testSensor - editedSensor['args']['channel'] = GPIO.instance.pins[5] + editedSensor['args']['channel'] = GPIO().pins[5] retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) self.assertTrue(retValue) retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) @@ -71,12 +70,12 @@ def testSensors(self): self.assertNotIn(testSensor['name'], deviceNames) def testSensorInfo(self): - actuator_channel = GPIO.instance.pins[9] - light_switch_channel = GPIO.instance.pins[9] + actuator_channel = GPIO().pins[9] + light_switch_channel = GPIO().pins[9] sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': actuator_channel}, 'name': 'test_actuator'}, 'light_switch' : {'description': 'Light Switch', 'device': 'LightSwitch', 'args': {'gpio': 'GPIO', 'invert': True, 'channel': light_switch_channel}, 'name': 'test_light_switch'}, - 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, - 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} + # 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, + # 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} } for sensor in sensors.values(): SensorsClientTest.client.AddSensor(sensor['name'], sensor['description'], sensor['device'], sensor['args']) @@ -104,12 +103,12 @@ def setSensorValue(self, sensor, value): def setChannelFunction(self, channel, function): SensorsClientTest.client.gpio.setFunctionString(channel, function) bus = SensorsClientTest.client.BusInfo() - self.assertEqual(function, bus['GPIO'][str(channel)]['function']) + self.assertEqual(function, bus['GPIO'][channel]['function']) def setChannelValue(self, channel, value): SensorsClientTest.client.gpio.digitalWrite(channel, value) bus = SensorsClientTest.client.BusInfo() - self.assertEqual(value, bus['GPIO'][str(channel)]['value']) + self.assertEqual(value, bus['GPIO'][channel]['value']) if __name__ == '__main__': setInfo() From 7e4e7079b468d45bc2eb04a09030db7e60f58cda Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 11 Aug 2017 12:26:55 -0600 Subject: [PATCH 044/129] Fix variable name conflict. --- myDevices/devices/digital/gpio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index b17fdcb..bb9803b 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -329,7 +329,7 @@ def __portWrite__(self, value): def wildcard(self, compact=False): if gpio_library and os.geteuid() != 0: #If not root on an ASUS device get the pin states as root - value, error = executeCommand('sudo python3 -m myDevices.devices.readvalue --pins') + value, err = executeCommand('sudo python3 -m myDevices.devices.readvalue --pins') value = value.splitlines()[0] import json return json.loads(value) From 999c6510dabc6442f43e369c2282f1df480c6a68 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 14 Aug 2017 16:38:34 -0600 Subject: [PATCH 045/129] Add BeagleBone I2C support. --- myDevices/devices/bus.py | 38 +++++++++++++++++++++++++------------- myDevices/devices/i2c.py | 5 ++++- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index 9cf3810..6031bc0 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -20,7 +20,31 @@ from myDevices.system.version import OS_VERSION, OS_JESSIE, OS_WHEEZY from myDevices.system.hardware import Hardware -if Hardware().getModel() != 'Tinker Board': +MODEL = Hardware().getModel() +if MODEL == 'Tinker Board': + # Tinker Board only supports I2C and SPI for now. These are enabled by default and + # don't need to load any modules. + BUSLIST = { + "I2C": { + "enabled": True, + }, + + "SPI": { + "enabled": True, + } + } +elif 'BeagleBone' in MODEL: + BUSLIST = { + "I2C": { + "enabled": True, + }, + + "SPI": { + "enabled": True, + } + } +else: + # Raspberry Pi BUSLIST = { "I2C": { "enabled": False, @@ -45,18 +69,6 @@ "modules": ["w1-gpio"], "wait": 2} } -else: - # Tinker Board only supports I2C and SPI for now. These are enabled by default and - # don't need to load any modules. - BUSLIST = { - "I2C": { - "enabled": True, - }, - - "SPI": { - "enabled": True, - } - } def loadModule(module): diff --git a/myDevices/devices/i2c.py b/myDevices/devices/i2c.py index cad866b..0321d13 100644 --- a/myDevices/devices/i2c.py +++ b/myDevices/devices/i2c.py @@ -52,8 +52,11 @@ def __init__(self, slave): raise Exception("SLAVE_ADDRESS_USED") self.channel = 0 - if BOARD_REVISION > 1 or Hardware().getModel() == 'Tinker Board': + model = Hardware().getModel() + if BOARD_REVISION > 1 or model == 'Tinker Board': self.channel = 1 + elif 'BeagleBone' in model: + self.channel = 2 Bus.__init__(self, "I2C", "/dev/i2c-%d" % self.channel) self.slave = slave From 48262f8f19e23f2f1b7e8ad9d86de4f6b5929b42 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 15 Aug 2017 17:47:07 -0600 Subject: [PATCH 046/129] Add BeagleBone SPI support. --- myDevices/devices/bus.py | 18 ++++++++++++++---- myDevices/devices/spi.py | 5 ++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index 6031bc0..42e52cf 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -40,7 +40,9 @@ }, "SPI": { - "enabled": True, + "enabled": False, + "gpio": {17:"SPI0_CS", 18:"SPI0_D1", 21:"SPI0_D1", 22:"SPI0_SCLK"}, + "configure_pin_command": "config-pin P9.{} spi" } } else: @@ -70,6 +72,16 @@ "wait": 2} } +def enableBus(bus): + loadModules(bus) + configurePins(bus) + BUSLIST[bus]["enabled"] = True + +def configurePins(bus): + if "configure_pin_command" in BUSLIST[bus]: + for pin in BUSLIST[bus]["gpio"].keys(): + command = BUSLIST[bus]["configure_pin_command"].format(pin) + subprocess.call(command.split(' ')) def loadModule(module): subprocess.call(["sudo", "modprobe", module]) @@ -86,8 +98,6 @@ def loadModules(bus): info("Sleeping %ds to let %s modules load" % (BUSLIST[bus]["wait"], bus)) time.sleep(BUSLIST[bus]["wait"]) - BUSLIST[bus]["enabled"] = True - def unloadModules(bus): info("Unloading %s modules" % bus) for module in BUSLIST[bus]["modules"]: @@ -122,7 +132,7 @@ def checkAllBus(): class Bus(): def __init__(self, busName, device, flag=os.O_RDWR): - loadModules(busName) + enableBus(busName) self.busName = busName self.device = device self.flag = flag diff --git a/myDevices/devices/spi.py b/myDevices/devices/spi.py index bf42a26..66cf875 100644 --- a/myDevices/devices/spi.py +++ b/myDevices/devices/spi.py @@ -87,8 +87,11 @@ def SPI_IOC_MESSAGE(count): class SPI(Bus): def __init__(self, chip=0, mode=0, bits=8, speed=0, init=True): bus = 0 - if Hardware().getModel() == 'Tinker Board': + model = Hardware().getModel() + if model == 'Tinker Board': bus = 2 + elif 'BeagleBone' in model: + bus = 1 Bus.__init__(self, "SPI", "/dev/spidev%d.%d" % (bus, chip)) self.chip = chip From f9f0a34ea22373743898ce00728197f7d18fb450 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 18 Aug 2017 12:24:38 -0600 Subject: [PATCH 047/129] Remove unused BeagleBone config options. --- myDevices/system/systemconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index e55be0e..a080200 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -30,7 +30,7 @@ def ExecuteConfigCommand(config_id, parameters): config_id: Id of command to run parameters: Parameters to use when executing command """ - if Hardware().getModel() == 'Tinker Board': + if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): return (1, 'Not supported') debug('SystemConfig::ExecuteConfigCommand') if config_id == 0: @@ -53,7 +53,7 @@ def RestartService(): def getConfig(): """Return dict containing configuration settings""" configItem = {} - if Hardware().getModel() == 'Tinker Board': + if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): return configItem try: (returnCode, output) = SystemConfig.ExecuteConfigCommand(17, '') From 4091bb8d80e75ab0f55a2bbd7ceb6d7d159196a3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 22 Aug 2017 15:55:11 -0600 Subject: [PATCH 048/129] Specify headers in BeagleBone pin mapping. --- myDevices/devices/digital/gpio.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index bb9803b..38e22a4 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -53,7 +53,6 @@ class NativeGPIO(Singleton, GPIOPort): def __init__(self): self.setPinMapping() - self.pins = [pin for pin in self.MAPPING if type(pin) is int] GPIOPort.__init__(self, max(self.pins) + 1) self.post_value = True self.post_function = True @@ -368,7 +367,9 @@ def setPinMapping(self): if model == 'Tinker Board': self.MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] elif 'BeagleBone' in model: - self.MAPPING = ["GND", "GND", "V33", "V33", "V50", "V50", "V50", "V50", "PWR", "RST", 30, 60, 31, 50, 48, 51, 5, 4, "I2C2_SCL", "I2C2_SDA", 3, 2, 49, 15, 117, 14, 115, "SPI1_CS0", "SPI1_D0", 112, "SPI1_CLK", "VDD_ADC", "AIN4", "GNDA_ADC", "AIN6", "AIN5", "AIN2", "AIN3", "AIN0", "AIN1", 20, 7, "GND", "GND", "GND", "GND", "GND", "GND", "MMC1_DAT6", "MMC1_DAT7", "MMC1_DAT2", "MMC1_DAT3", 66, 67, 69, 68, 45, 44, 23, 26, 47, 46, 27, 65, 22, "MMC1_CMD", "MMC1_CLK", "MMC1_DAT5", "MMC1_DAT4", "MMC1_DAT1", "MMC1_DAT0", 61, "LCD_VSYNC", "LCD_PCLK", "LCD_HSYNC", "LCD_ACBIAS", "LCD_DATA14", "LCD_DATA15", "LCD_DATA13", "LCD_DATA11", "LCD_DATA12", "LCD_DATA10", "LCD_DATA8", "LCD_DATA9", "LCD_DATA6", "LCD_DATA7", "LCD_DATA4", "LCD_DATA5", "LCD_DATA2", "LCD_DATA3", "LCD_DATA0", "LCD_DATA1"] + self.MAPPING = {"headers": {"P9": ["GND", "GND", "V33", "V33", "V50", "V50", "V50", "V50", "PWR", "RST", 30, 60, 31, 50, 48, 51, 5, 4, "I2C2_SCL", "I2C2_SDA", 3, 2, 49, 15, 117, 14, 115, "SPI1_CS0", "SPI1_D0", 112, "SPI1_CLK", "VDD_ADC", "AIN4", "GNDA_ADC", "AIN6", "AIN5", "AIN2", "AIN3", "AIN0", "AIN1", 20, 7, "GND", "GND", "GND", "GND"], + "P8": ["GND", "GND", "MMC1_DAT6", "MMC1_DAT7", "MMC1_DAT2", "MMC1_DAT3", 66, 67, 69, 68, 45, 44, 23, 26, 47, 46, 27, 65, 22, "MMC1_CMD", "MMC1_CLK", "MMC1_DAT5", "MMC1_DAT4", "MMC1_DAT1", "MMC1_DAT0", 61, "LCD_VSYNC", "LCD_PCLK", "LCD_HSYNC", "LCD_ACBIAS", "LCD_DATA14", "LCD_DATA15", "LCD_DATA13", "LCD_DATA11", "LCD_DATA12", "LCD_DATA10", "LCD_DATA8", "LCD_DATA9", "LCD_DATA6", "LCD_DATA7", "LCD_DATA4", "LCD_DATA5", "LCD_DATA2", "LCD_DATA3", "LCD_DATA0", "LCD_DATA1"]}, + "order": ["P9", "P8"]} else: if BOARD_REVISION == 1: self.MAPPING = ["V33", "V50", 0, "V50", 1, "GND", 4, 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] @@ -376,3 +377,9 @@ def setPinMapping(self): self.MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] elif BOARD_REVISION == 3: self.MAPPING = ["V33", "V50", 2, "V50", 3, "GND", 4, 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] + if isinstance(self.MAPPING, list): + self.pins = [pin for pin in self.MAPPING if type(pin) is int] + elif 'headers' in self.MAPPING: + self.pins = [] + for header in self.MAPPING['headers'].values(): + self.pins.extend([pin for pin in header if type(pin) is int]) From 61ca23c6075b8c53f25eae1f82800176f57ce754 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 22 Aug 2017 16:00:15 -0600 Subject: [PATCH 049/129] Use sudo to launch config-pin so it can be run from a non-root process. --- myDevices/devices/bus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index 42e52cf..b44c30f 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -42,7 +42,7 @@ "SPI": { "enabled": False, "gpio": {17:"SPI0_CS", 18:"SPI0_D1", 21:"SPI0_D1", 22:"SPI0_SCLK"}, - "configure_pin_command": "config-pin P9.{} spi" + "configure_pin_command": "sudo config-pin P9.{} spi" } } else: From 22b0ea827f3de4a7960dd26176f3c808778098be Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 11 Sep 2017 16:39:29 -0600 Subject: [PATCH 050/129] Updated comment. --- myDevices/system/systeminfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index c450bc9..7467c23 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -87,7 +87,7 @@ def getUptime(self): return info def getDiskInfo(self): - """Get system uptime as a dict + """Get disk usage info as a dict Returned dict example:: From 96eb53c910136a232477259c0c9762ff449b3a46 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 11 Sep 2017 17:46:11 -0600 Subject: [PATCH 051/129] Run scheduled actions when agent is disconnected. --- myDevices/cloud/client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ddfeee5..100d0ee 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -268,6 +268,7 @@ def __init__(self, host, port, cayenneApiHost): self.installDate = int(time()) self.config.set('Agent', 'InstallDate', self.installDate) self.networkConfig = Config(NETWORK_SETTINGS) + self.sensorsClient = sensors.SensorsClient() self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.Initialize() self.CheckSubscription() @@ -301,7 +302,6 @@ def Initialize(self): self.count = 10000 self.buff = bytearray(self.count) #start thread only after init of other fields - self.sensorsClient = sensors.SensorsClient() self.sensorsClient.SetDataChanged(self.OnDataChanged, self.BuildPT_SYSTEM_INFO) self.processManager = services.ProcessManager() self.serviceManager = services.ServiceManager() @@ -471,6 +471,7 @@ def CheckSubscription(self): info('Registration succeeded for invite code {}, auth id = {}'.format(inviteCode, authId)) self.config.set('Agent', 'Initialized', 'true') self.MachineId = authId + self.config.set('Agent', 'Id', self.MachineId) @property def Start(self): @@ -622,9 +623,12 @@ def ReadMessage(self): def RunAction(self, action): """Run a specified action""" debug('RunAction') - if 'MachineName' in action and self.MachineId != action['MachineName']: - debug('Scheduler action is not assigned for this machine: ' + str(action)) - return + if 'MachineName' in action: + #Use the config file machine if self.MachineId has not been set yet due to connection issues + machine_id = self.MachineId if self.MachineId else self.config.get('Agent', 'Id') + if machine_id != action['MachineName']: + debug('Scheduler action is not assigned for this machine: ' + str(action)) + return self.ExecuteMessage(action) def SendNotification(self, notify, subject, body): From 5f8664c3d6fbb39967ce36e96b8982db92726131 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 20 Sep 2017 18:29:48 -0600 Subject: [PATCH 052/129] Send/receive packets using MQTT. --- myDevices/__main__.py | 4 +- myDevices/cloud/cayennemqtt.py | 176 ++++++++++++++++++++ myDevices/cloud/client.py | 257 +++++++++++++++++++++++------ myDevices/test/cayennemqtt_test.py | 72 ++++++++ 4 files changed, 454 insertions(+), 55 deletions(-) create mode 100644 myDevices/cloud/cayennemqtt.py create mode 100644 myDevices/test/cayennemqtt_test.py diff --git a/myDevices/__main__.py b/myDevices/__main__.py index c8f645b..a5f0e15 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -122,8 +122,8 @@ def main(argv): writePidToFile(pidfile) logToFile(logfile) config = Config(configfile) - HOST = config.get('CONFIG', 'ServerAddress', 'cloud.mydevices.com') - PORT = config.getInt('CONFIG', 'ServerPort', 8181) + HOST = config.get('CONFIG', 'ServerAddress', 'mqtt.mydevices.com') + PORT = config.getInt('CONFIG', 'ServerPort', 1883) CayenneApiHost = config.get('CONFIG', 'CayenneApi', 'https://api.mydevices.com') global client client = CloudServerClient(HOST, PORT, CayenneApiHost) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py new file mode 100644 index 0000000..5431323 --- /dev/null +++ b/myDevices/cloud/cayennemqtt.py @@ -0,0 +1,176 @@ +import time +from json import loads +from ssl import PROTOCOL_TLSv1_2 +import paho.mqtt.client as mqtt +from myDevices.utils.logger import debug, error, exception, info, logJson, warn + +# Topics +DATA_TOPIC = "data" +COMMAND_TOPIC = "cmd" + +class CayenneMQTTClient: + """Cayenne MQTT Client class. + + This is the main client class for connecting to Cayenne and sending and receiving data. + + Standard usage: + * Set on_message callback, if you are receiving data. + * Connect to Cayenne using the begin() function. + * Call loop() at intervals (or loop_forever() once) to perform message processing. + * Send data to Cayenne using write functions: virtualWrite(), celsiusWrite(), etc. + * Receive and process data from Cayenne in the on_message callback. + + The on_message callback can be used by creating a function and assigning it to CayenneMQTTClient.on_message member. + The callback function should have the following signature: on_message(topic, message) + If it exists this callback is used as the default message handler. + + The on_command callback can be used by creating a function and assigning it to CayenneMQTTClient.on_message member. + The callback function should have the following signature: on_command(channel, value) + If it exists this callback is used as the command message handler. + """ + client = None + rootTopic = "" + connected = False + on_message = None + on_command = None + + def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', port=1883): + """Initializes the client and connects to Cayenne. + + username is the Cayenne username. + password is the Cayenne password. + clientID is the Cayennne client ID for the device. + hostname is the MQTT broker hostname. + port is the MQTT broker port. + """ + self.rootTopic = "v0.9/%s/things/%s" % (username, clientid) + self.client = mqtt.Client(client_id=clientid, clean_session=True, userdata=self) + self.client.on_connect = self.connect_callback + self.client.on_disconnect = self.disconnect_callback + self.client.on_message = self.message_callback + self.client.username_pw_set(username, password) + if port == 8883: + self.client.tls_set(ca_certs="/etc/mosquitto/certs/test-ca.crt", certfile="/etc/mosquitto/certs/cli2.crt", + keyfile="/etc/mosquitto/certs/cli2.key", tls_version=PROTOCOL_TLSv1_2) + self.client.connect(hostname, port, 60) + info("Connecting to %s..." % hostname) + + def connect_callback(self, client, userdata, flags, rc): + """The callback for when the client connects to the server. + + client is the client instance for this callback. + userdata is the private user data as set in Client() or userdata_set(). + flags are the response flags sent by the broker. + rc is the connection result. + """ + if rc != 0: + # MQTT broker error codes + broker_errors = { + 1 : 'unacceptable protocol version', + 2 : 'identifier rejected', + 3 : 'server unavailable', + 4 : 'bad user name or password', + 5 : 'not authorized', + } + raise Exception("Connection failed, " + broker_errors.get(rc, "result code " + str(rc))) + else: + info("Connected with result code "+str(rc)) + self.connected = True + # Subscribing in on_connect() means that if we lose the connection and + # reconnect then subscriptions will be renewed. + client.subscribe(self.get_topic_string(COMMAND_TOPIC)) + + def disconnect_callback(self, client, userdata, rc): + """The callback for when the client disconnects from the server. + + client is the client instance for this callback. + userdata is the private user data as set in Client() or userdata_set(). + rc is the connection result. + """ + info("Disconnected with result code "+str(rc)) + self.connected = False + reconnected = False + while not reconnected: + try: + self.client.reconnect() + reconnected = True + except: + print("Reconnect failed, retrying") + time.sleep(5) + + def message_callback(self, client, userdata, msg): + """The callback for when a message is received from the server. + + client is the client instance for this callback. + userdata is the private user data as set in Client() or userdata_set(). + msg is the received message. + """ + info('message_callback') + try: + if msg.topic.startswith(self.rootTopic): + topic = msg.topic[len(self.rootTopic) + 1:] + message = loads(msg.payload.decode()) + debug('message_callback: {} {}'.format(topic, message)) + if self.on_message: + self.on_message(topic, message) + except: + exception("Couldn't process: "+msg.topic+" "+str(msg.payload)) + + def get_topic_string(self, topic, append_wildcard=False): + """Return a topic string. + + topic: the topic substring + append_wildcard: if True append the single level topics wildcard (+)""" + if append_wildcard: + return '{}/{}/+'.format(self.rootTopic, topic) + else: + return '{}/{}'.format(self.rootTopic, topic) + + def loop(self, timeout=1.0): + """Process Cayenne messages. + + This should be called regularly to ensure Cayenne messages are sent and received. + + timeout: The time in seconds to wait for incoming/outgoing network + traffic before timing out and returning. + """ + self.client.loop(timeout) + + def loop_start(self): + """This is part of the threaded client interface. Call this once to + start a new thread to process network traffic. This provides an + alternative to repeatedly calling loop() yourself. + """ + self.client.loop_start() + + def loop_stop(self): + """This is part of the threaded client interface. Call this once to + stop the network thread previously created with loop_start(). This call + will block until the network thread finishes. + """ + self.client.loop_stop() + + def publish_packet(self, topic, packet, qos=0, retain=False): + """Publish a packet. + + topic: topic substring. + packet: JSON packet to publish. + qos: quality of service level to use. + retain: if True, the message will be set as the "last known good"/retained message for the topic. + """ + debug('Publish to {}'.format(self.get_topic_string(topic))) + self.client.publish(self.get_topic_string(topic), packet, qos, retain) + + def publish_response(self, msg_id, error_message): + """Send a command response to Cayenne. + + This should be sent when a command message has been received. + msg_id is the ID of the message received. + error_message is the error message to send. This should be set to None if there is no error. + """ + topic = self.get_topic_string(RESPONSE_TOPIC) + if error_message: + payload = "error,%s=%s" % (msg_id, error_message) + else: + payload = "ok,%s" % (msg_id) + self.client.publish(topic, payload) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 100d0ee..6dddc4d 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -28,7 +28,7 @@ from select import select from hashlib import sha256 from myDevices.cloud.apiclient import CayenneApiClient - +import myDevices.cloud.cayennemqtt as cayennemqtt NETWORK_SETTINGS = '/etc/myDevices/Network.ini' APP_SETTINGS = '/etc/myDevices/AppSettings.ini' @@ -194,13 +194,37 @@ def run(self): while self.Continue: sleep(GENERAL_SLEEP_THREAD) try: - if not self.cloudClient.connected: + if self.cloudClient.mqttClient.connected == False: + info('WriterThread mqttClient not connected') continue message = self.cloudClient.DequeuePacket() if not message: + info('WriterThread mqttClient no message, {}'.format(message)) continue + # json_data = dumps(message) # + '\n' + info('WriterThread, topic: {} {}'.format(cayennemqtt.DATA_TOPIC, type(message))) + self.cloudClient.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, message) self.cloudClient.SendMessage(message) - del message + # # topic = self.cloudClient.topics.get(int(message['PacketType'])) + # topic = '{}/{}'.format(cayennemqtt.INTERNAL_RPI_DATA_TOPIC, self.cloudClient.MachineId) + # topic = cayennemqtt.INTERNAL_RPI_DATA_TOPIC + # info('WriterThread, type: {}, topic: {} {}'.format(int(message['PacketType']), topic, json_data)) + # if topic: + # #Debug hack to handle the fact that some packet types are used for both input and output. + # #This should be changed when completely switched over to MQTT topics so the same topics aren't used for input and output. + # if topic is cayennemqtt.SETTINGS_JSON_TOPIC: # If remove sensor response, use internal sensor update topic. + # topic = cayennemqtt.INTERNAL_SENSOR_RESPONSE_TOPIC + # if topic is cayennemqtt.INTERNAL_JSON_TOPIC: + # topic = cayennemqtt.INTERNAL_REMOTE_RESPONSE_TOPIC + # if topic is cayennemqtt.SYSTEM_JSON_TOPIC and not 'IpAddress' in message: # If variable system info, use data topic. + # topic = cayennemqtt.DATA_JSON_TOPIC + # self.cloudClient.mqttClient.publish_packet(topic, json_data) + # if int(message['PacketType']) != PacketTypes.PT_REQUEST_SCHEDULES.value: # Remove when completely switched to MQTT + # self.cloudClient.SendMessage(json_data) + # packet = dumps(message, sort_keys=True) + '\n' + # self.cloudClient.mqttClient.publish_packet("packets", packet) + # del message + # del json_data message = None except: exception("WriterThread Unexpected error") @@ -269,9 +293,14 @@ def __init__(self, host, port, cayenneApiHost): self.config.set('Agent', 'InstallDate', self.installDate) self.networkConfig = Config(NETWORK_SETTINGS) self.sensorsClient = sensors.SensorsClient() + self.MachineId = None + self.username = None + self.password = None + self.clientId = None + self.CheckSubscription() + #self.defaultRDServer = self.networkConfig.get('CONFIG','RemoteDesktopServerAddress') self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.Initialize() - self.CheckSubscription() self.FirstRun() self.updater = Updater(self.config) self.updater.start() @@ -295,14 +324,14 @@ def Initialize(self): self.hardware = Hardware() self.oSInfo = OSInfo() self.downloadSpeed = DownloadSpeed(self.config) - self.MachineId = None + self.downloadSpeed.getDownloadSpeed() self.connected = False self.exiting = False self.Start self.count = 10000 self.buff = bytearray(self.count) #start thread only after init of other fields - self.sensorsClient.SetDataChanged(self.OnDataChanged, self.BuildPT_SYSTEM_INFO) + self.sensorsClient.SetDataChanged(self.OnDataChanged, self.SendSystemState) self.processManager = services.ProcessManager() self.serviceManager = services.ServiceManager() self.wifiManager = WifiManager.WifiManager() @@ -313,6 +342,8 @@ def Initialize(self): self.processorThread = ProcessorThread('processor', self) self.processorThread.start() TimerThread(self.CheckConnectionAndPing, self.pingRate) + TimerThread(self.SendSystemState, 30, 5) + self.previousSystemInfo = None self.sentHistoryData = {} self.historySendFails = 0 self.historyThread = Thread(target=self.SendHistoryData) @@ -342,10 +373,72 @@ def Destroy(self): def FirstRun(self): """Send messages when client is first started""" - self.BuildPT_SYSTEM_INFO() - self.RequestSchedules() + self.SendSystemInfo() + # self.RequestSchedules() + # self.BuildPT_LOCATION() + # def BuildPT_LOCATION(self): + # data = {} + # data['position'] = {} + # data['position']['latitude'] = '30.022112' + # data['position']['longitude'] = '45.022112' + # data['position']['accuracy'] = '20' + # data['position']['method'] = 'Windows location provider' + # data['provider'] = 'other' + # data['time'] = int(time()) + # data['PacketType'] = PacketTypes.PT_LOCATION.value + # data['MachineName'] = self.MachineId + # self.EnqueuePacket(data) + + def OnDataChanged(self, raspberryValue): + """Enqueue a packet containing changed system data to send to the server""" + data = {} + data['MachineName'] = self.MachineId + data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value + data['Timestamp'] = int(time()) + data['RaspberryInfo'] = raspberryValue + self.EnqueuePacket(data) + del data + del raspberryValue + + def SendSystemInfo(self): + """Enqueue a packet containing system info to send to the server""" + try: + # debug('SendSystemInfo') + data = {} + data['MachineName'] = self.MachineId + data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value + data['IpAddress'] = self.PublicIP + data['GatewayMACAddress'] = self.hardware.getMac() + raspberryValue = {} + # raspberryValue['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) + raspberryValue['AntiVirus'] = 'None' + raspberryValue['Firewall'] = 'iptables' + raspberryValue['FirewallEnabled'] = 'true' + raspberryValue['ComputerMake'] = self.hardware.getManufacturer() + raspberryValue['ComputerModel'] = self.hardware.getModel() + raspberryValue['OsName'] = self.oSInfo.ID + raspberryValue['OsBuild'] = self.oSInfo.ID_LIKE + raspberryValue['OsArchitecture'] = self.hardware.Revision + raspberryValue['OsVersion'] = self.oSInfo.VERSION_ID + raspberryValue['ComputerName'] = self.machineName + raspberryValue['AgentVersion'] = self.config.get('Agent','Version') + raspberryValue['GatewayMACAddress'] = self.hardware.getMac() + raspberryValue['OsSettings'] = RaspiConfig.getConfig() + raspberryValue['NetworkId'] = WifiManager.Network.GetNetworkId() + raspberryValue['WifiStatus'] = self.wifiManager.GetStatus() + data['RaspberryInfo'] = raspberryValue + if data != self.previousSystemInfo: + self.previousSystemInfo = data.copy() + data['Timestamp'] = int(time()) + self.EnqueuePacket(data) + logJson('SendSystemInfo: ' + dumps(data), 'SendSystemInfo') + del raspberryValue + del data + data=None + except Exception as e: + exception('SendSystemInfo unexpected error: ' + str(e)) - def BuildPT_UTILIZATION(self): + def SendSystemUtilization(self): """Enqueue a packet containing system utilization data to send to the server""" data = {} data['MachineName'] = self.MachineId @@ -361,20 +454,12 @@ def BuildPT_UTILIZATION(self): data['PercentProcessorTime'] = self.processManager.PercentProcessorTime self.EnqueuePacket(data) - def OnDataChanged(self, raspberryValue): - """Enqueue a packet containing system utilization data to send to the server""" - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value - data['Timestamp'] = int(time()) - data['RaspberryInfo'] = raspberryValue - self.EnqueuePacket(data) - del data - del raspberryValue - - def BuildPT_SYSTEM_INFO(self): + def SendSystemState(self): """Enqueue a packet containing system information to send to the server""" try: + # debug('SendSystemState') + self.SendSystemInfo() + self.SendSystemUtilization() data = {} data['MachineName'] = self.MachineId data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value @@ -463,45 +548,63 @@ def CheckSubscription(self): """Check that an invite code is valid""" inviteCode = self.config.get('Agent', 'InviteCode') cayenneApiClient = CayenneApiClient(self.CayenneApiHost) - authId = cayenneApiClient.loginDevice(inviteCode) - if authId == None: + credentials = cayenneApiClient.loginDevice(inviteCode) + if credentials == None: error('Registration failed for invite code {}, closing the process'.format(inviteCode)) Daemon.Exit() else: - info('Registration succeeded for invite code {}, auth id = {}'.format(inviteCode, authId)) + info('Registration succeeded for invite code {}, credentials = {}'.format(inviteCode, credentials)) self.config.set('Agent', 'Initialized', 'true') - self.MachineId = authId - self.config.set('Agent', 'Id', self.MachineId) - + try: + self.MachineId = credentials#credentials['id'] + # self.username = credentials['mqtt']['username'] + # self.password = credentials['mqtt']['password'] + # self.clientId = credentials['mqtt']['clientId'] + self.config.set('Agent', 'Id', self.MachineId) + except: + exception('Invalid credentials, closing the process') + Daemon.Exit() + info('CheckSubscription: MachineId {}'.format(self.MachineId)) + @property def Start(self): - """Connect to the server""" + #debug('Start') if self.connected: ret = False error('Start already connected') else: - info('Connecting to: {}:{}'.format(self.HOST, self.PORT)) count = 0 with self.mutex: - count += 1 + count+=1 while self.connected == False and count < 30: try: - self.sock = None + self.sock = None self.wrappedSocket = None + ##debug('Start wrap_socket') self.sock = socket(AF_INET, SOCK_STREAM) + #self.wrappedSocket = wrap_socket(self.sock, ca_certs="/etc/myDevices/ca.crt", cert_reqs=CERT_REQUIRED) self.wrappedSocket = wrap_socket(self.sock) - self.wrappedSocket.connect((self.HOST, self.PORT)) + self.wrappedSocket.connect(('cloud.mydevices.com', 8181)) info('myDevices cloud connected') + self.mqttClient = cayennemqtt.CayenneMQTTClient() + self.mqttClient.on_message = self.OnMessage + self.mqttClient.on_command = self.OnCommand + username = self.config.get('Agent', 'Username') + password = self.config.get('Agent', 'Password') + clientID = self.config.get('Agent', 'ClientID') + self.mqttClient.begin(username, password, clientID, self.HOST, self.PORT) + self.mqttClient.loop_start() self.connected = True except socket_error as serr: Daemon.OnFailure('cloud', serr.errno) - error('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(serr)) + error ('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(serr)) self.connected = False sleep(30-count) return self.connected def Stop(self): """Disconnect from the server""" + #debug('Stop started') Daemon.Reset('cloud') ret = True if self.connected == False: @@ -512,12 +615,14 @@ def Stop(self): try: self.wrappedSocket.shutdown(SHUT_RDWR) self.wrappedSocket.close() + self.mqttClient.loop_stop() info('myDevices cloud disconnected') except socket_error as serr: debug(str(serr)) - error('myDevices cloud disconnected error:' + str(serr)) + error ('myDevices cloud disconnected error:' + str(serr)) ret = False self.connected = False + #debug('Stop finished') return ret def Restart(self): @@ -575,46 +680,87 @@ def CheckJson(self, message): return False return True + def OnMessage(self, topic, message): + """Add message from the server to the queue""" + info('OnMessage: {}'.format(message)) + self.readQueue.put(message) + + def OnCommand(self, channel, value): + """Handle command message from the server""" + info('OnCommand: channel {}, value {}'.format(channel, value)) + try: + channel = int(channel) + value = int(value) + except ValueError: + pass + retValue = str(self.sensorsClient.GpioCommand('value', 'POST', channel, value)) + if retValue == str(value): + return None + else: + return retValue + def ReadMessage(self): - """Read a message from the server""" + """Read a message from the server and add it to the queue""" ret = True if self.connected == False: - ret = False + ret = False else: try: - self.count = 4096 - timeout_in_seconds = 10 + self.count=4096 + timeout_in_seconds=10 ready = select([self.wrappedSocket], [], [], timeout_in_seconds) if ready[0]: message = self.wrappedSocket.recv(self.count).decode() buffering = len(message) == 4096 while buffering and message: - if self.CheckJson(message): - buffering = False - else: - more = self.wrappedSocket.recv(self.count).decode() - if not more: - buffering = False - else: - message += more + if self.CheckJson(message): + buffering = False + else: + more = self.wrappedSocket.recv(self.count).decode() + if not more: + buffering = False + else: + message += more try: if message: messageObject = loads(message) + # topic = self.topics.get(int(messageObject['PacketType'])) + topic = cayennemqtt.COMMAND_TOPIC #'{}/{}'.format(cayennemqtt.COMMAND_TOPIC, self.MachineId) + if messageObject: + info('ReadMessage, type: {}, topic: {}'.format(int(messageObject['PacketType']), topic)) + # if topic: + # #Debug hack for testing. This should be removed when completely switched over to MQTT. + # if topic is cayennemqtt.COMMAND_JSON_TOPIC: + # info('ReadMessage, Service: {}'.format(messageObject['Service'])) + # if messageObject['Service'] == 'gpio': + # info('ReadMessage, {}'.format(messageObject)) + # info('ReadMessage, check success: {}'.format(messageObject['Service'])) + # topic = 'cmd/{}'.format(messageObject['Parameters']['Channel']) + # message = '{},{}'.format(messageObject['Id'], messageObject['Parameters']['Value']) + # info('ReadMessage, topic: {}, message {}'.format(topic, message)) + # self.mqttClient.publish_packet(topic, message) + # elif topic is cayennemqtt.SETTINGS_SCHEDULE_JSON_TOPIC: + # self.mqttClient.publish_packet(topic, message, 1, True) + # elif topic is not cayennemqtt.SYSTEM_JSON_TOPIC and topic is not cayennemqtt.DATA_JSON_TOPIC: + # self.mqttClient.publish_packet(topic, message) + self.mqttClient.publish_packet(cayennemqtt.COMMAND_TOPIC, message) + else: + self.readQueue.put(messageObject) del message - self.readQueue.put(messageObject) else: error('ReadMessage received empty message string') except: - exception('ReadMessage error: ' + str(message)) + exception('ReadMessage error: ' + str(message)) return False Daemon.Reset('cloud') except IOError as ioerr: debug('IOError: ' + str(ioerr)) self.Restart() + #Daemon.OnFailure('cloud', ioerr.errno) except socket_error as serr: Daemon.OnFailure('cloud', serr.errno) except: - exception('ReadMessage error') + exception('ReadMessage error') ret = False sleep(1) Daemon.OnFailure('cloud') @@ -680,11 +826,12 @@ def ExecuteMessage(self, messageObject): info("ExecuteMessage: " + str(messageObject['PacketType'])) packetType = int(messageObject['PacketType']) if packetType == PacketTypes.PT_UTILIZATION.value: - self.BuildPT_UTILIZATION() + self.SendSystemUtilization() info(PacketTypes.PT_UTILIZATION) return if packetType == PacketTypes.PT_SYSTEM_INFO.value: - self.BuildPT_SYSTEM_INFO() + info("ExecuteMessage - sysinfo - Calling SendSystemState") + self.SendSystemState() info(PacketTypes.PT_SYSTEM_INFO) return if packetType == PacketTypes.PT_UNINSTALL_AGENT.value: @@ -706,7 +853,11 @@ def ExecuteMessage(self, messageObject): if packetType == PacketTypes.PT_PRODUCT_INFO.value: self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) info(PacketTypes.PT_PRODUCT_INFO) - return + return + # if packetType == PacketTypes.PT_START_RDS_LOCAL_INIT.value: + # error('PT_START_RDS_LOCAL_INIT not implemented') + # info(PacketTypes.PT_START_RDS_LOCAL_INIT) + # return if packetType == PacketTypes.PT_RESTART_COMPUTER.value: info(PacketTypes.PT_RESTART_COMPUTER) data = {} @@ -945,7 +1096,7 @@ def ProcessDeviceCommand(self, messageObject): def EnqueuePacket(self, message): """Enqueue a message packet to send to the server""" message['PacketTime'] = GetTime() - json_data = dumps(message)+ '\n' + json_data = dumps(message) + '\n' message = None self.writeQueue.put(json_data) diff --git a/myDevices/test/cayennemqtt_test.py b/myDevices/test/cayennemqtt_test.py new file mode 100644 index 0000000..6c4b521 --- /dev/null +++ b/myDevices/test/cayennemqtt_test.py @@ -0,0 +1,72 @@ +import unittest +import myDevices.cloud.cayennemqtt as cayennemqtt +import paho.mqtt.client as mqtt +from time import sleep +from json import dumps, loads + +TEST_USERNAME = "user" +TEST_PASSWORD = "password" +TEST_CLIENT_ID = "id" +TEST_HOST = "localhost" +TEST_PORT = 1883 + +class CayenneMQTTTest(unittest.TestCase): + def setUp(self): + # print('setUp') + self.mqttClient = cayennemqtt.CayenneMQTTClient() + self.mqttClient.on_message = self.OnMessage + self.mqttClient.on_command = self.OnCommand + self.mqttClient.begin(TEST_USERNAME, TEST_PASSWORD, TEST_CLIENT_ID, TEST_HOST, TEST_PORT) + self.mqttClient.loop_start() + self.testClient = mqtt.Client("testID") + self.testClient.on_message = self.OnTestMessage + # self.testClient.on_log = self.OnTestLog + self.testClient.username_pw_set("testClient", "testClientPass") + self.testClient.connect(TEST_HOST, TEST_PORT, 60) + (result, messageID) = self.testClient.subscribe(self.mqttClient.get_topic_string(cayennemqtt.DATA_TOPIC)) + self.assertEqual(result, mqtt.MQTT_ERR_SUCCESS) + self.testClient.loop_start() + + def tearDown(self): + # print('tearDown') + self.mqttClient.loop_stop() + self.testClient.loop_stop() + + def OnMessage(self, topic, message): + self.receivedTopic = self.mqttClient.get_topic_string(topic) + self.receivedMessage = message + # print('OnMessage: {} {}'.format(self.receivedTopic, self.receivedMessage)) + + def OnCommand(self, channel, value): + # print('OnCommand: {} {}'.format(channel, value)) + return None + + def OnTestMessage(self, client, userdata, message): + self.receivedTopic = message.topic + self.receivedMessage = message.payload.decode() + # print('OnTestMessage: {} {}'.format(self.receivedTopic, self.receivedMessage)) + + def OnTestLog(self, client, userdata, level, buf): + print('OnTestLog: {}'.format(buf)) + + def testPublish(self): + # print('testPublish') + sentTopic = self.mqttClient.get_topic_string(cayennemqtt.DATA_TOPIC) + sentMessage = '{"publish_test":"data"}' + self.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, sentMessage) + sleep(0.5) + self.assertEqual(sentTopic, self.receivedTopic) + self.assertEqual(sentMessage, self.receivedMessage) + + def testCommand(self): + # print('testCommand') + sentTopic = self.mqttClient.get_topic_string(cayennemqtt.COMMAND_TOPIC) + sentMessage = '{"command_test":"data"}' + self.testClient.publish(sentTopic, sentMessage) + sleep(0.5) + sentMessage = loads(sentMessage) + self.assertEqual(sentTopic, self.receivedTopic) + self.assertEqual(sentMessage, self.receivedMessage) + +if __name__ == "__main__": + unittest.main() From 97ea5f11002799297ce0162884511294ff7b40df Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 21 Sep 2017 17:38:11 -0600 Subject: [PATCH 053/129] Remove cloud server code. --- myDevices/cloud/cayennemqtt.py | 7 +- myDevices/cloud/client.py | 263 +++-------------------------- myDevices/test/cayennemqtt_test.py | 5 - 3 files changed, 27 insertions(+), 248 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 5431323..7bfebfb 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -23,16 +23,11 @@ class CayenneMQTTClient: The on_message callback can be used by creating a function and assigning it to CayenneMQTTClient.on_message member. The callback function should have the following signature: on_message(topic, message) If it exists this callback is used as the default message handler. - - The on_command callback can be used by creating a function and assigning it to CayenneMQTTClient.on_message member. - The callback function should have the following signature: on_command(channel, value) - If it exists this callback is used as the command message handler. """ client = None rootTopic = "" connected = False on_message = None - on_command = None def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', port=1883): """Initializes the client and connects to Cayenne. @@ -105,8 +100,8 @@ def message_callback(self, client, userdata, msg): userdata is the private user data as set in Client() or userdata_set(). msg is the received message. """ - info('message_callback') try: + topic = msg.topic if msg.topic.startswith(self.rootTopic): topic = msg.topic[len(self.rootTopic) + 1:] message = loads(msg.payload.decode()) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 6dddc4d..e53145a 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -122,35 +122,6 @@ def __init__(self): exception("OSInfo Unexpected error") -class ReaderThread(Thread): - """Class for reading data from the server on a thread""" - - def __init__(self, name, client): - """Initialize reader thread""" - debug('ReaderThread init') - Thread.__init__(self, name=name) - self.cloudClient = client - self.Continue = True - - def run(self): - """Read messages from the server until the thread is stopped""" - debug('ReaderThread run, continue: ' + str(self.Continue)) - while self.Continue: - try: - sleep(GENERAL_SLEEP_THREAD) - if not self.cloudClient.connected: - continue - self.cloudClient.ReadMessage() - except: - exception("ReaderThread Unexpected error") - return - - def stop(self): - """Stop reading messages from the server""" - debug('ReaderThread stop') - self.Continue = False - - class ProcessorThread(Thread): """Class for processing messages from the server on a thread""" @@ -201,30 +172,8 @@ def run(self): if not message: info('WriterThread mqttClient no message, {}'.format(message)) continue - # json_data = dumps(message) # + '\n' - info('WriterThread, topic: {} {}'.format(cayennemqtt.DATA_TOPIC, type(message))) + debug('WriterThread, topic: {} {}'.format(cayennemqtt.DATA_TOPIC, type(message))) self.cloudClient.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, message) - self.cloudClient.SendMessage(message) - # # topic = self.cloudClient.topics.get(int(message['PacketType'])) - # topic = '{}/{}'.format(cayennemqtt.INTERNAL_RPI_DATA_TOPIC, self.cloudClient.MachineId) - # topic = cayennemqtt.INTERNAL_RPI_DATA_TOPIC - # info('WriterThread, type: {}, topic: {} {}'.format(int(message['PacketType']), topic, json_data)) - # if topic: - # #Debug hack to handle the fact that some packet types are used for both input and output. - # #This should be changed when completely switched over to MQTT topics so the same topics aren't used for input and output. - # if topic is cayennemqtt.SETTINGS_JSON_TOPIC: # If remove sensor response, use internal sensor update topic. - # topic = cayennemqtt.INTERNAL_SENSOR_RESPONSE_TOPIC - # if topic is cayennemqtt.INTERNAL_JSON_TOPIC: - # topic = cayennemqtt.INTERNAL_REMOTE_RESPONSE_TOPIC - # if topic is cayennemqtt.SYSTEM_JSON_TOPIC and not 'IpAddress' in message: # If variable system info, use data topic. - # topic = cayennemqtt.DATA_JSON_TOPIC - # self.cloudClient.mqttClient.publish_packet(topic, json_data) - # if int(message['PacketType']) != PacketTypes.PT_REQUEST_SCHEDULES.value: # Remove when completely switched to MQTT - # self.cloudClient.SendMessage(json_data) - # packet = dumps(message, sort_keys=True) + '\n' - # self.cloudClient.mqttClient.publish_packet("packets", packet) - # del message - # del json_data message = None except: exception("WriterThread Unexpected error") @@ -316,10 +265,6 @@ def Initialize(self): self.mutex = RLock() self.readQueue = Queue() self.writeQueue = Queue() - self.pingRate = 10 - self.pingTimeout = 35 - self.waitPing = 0 - self.lastPing = time()-self.pingRate - 1 self.PublicIP = ipgetter.myip() self.hardware = Hardware() self.oSInfo = OSInfo() @@ -337,11 +282,8 @@ def Initialize(self): self.wifiManager = WifiManager.WifiManager() self.writerThread = WriterThread('writer', self) self.writerThread.start() - self.readerThread = ReaderThread('reader', self) - self.readerThread.start() self.processorThread = ProcessorThread('processor', self) self.processorThread.start() - TimerThread(self.CheckConnectionAndPing, self.pingRate) TimerThread(self.SendSystemState, 30, 5) self.previousSystemInfo = None self.sentHistoryData = {} @@ -556,10 +498,10 @@ def CheckSubscription(self): info('Registration succeeded for invite code {}, credentials = {}'.format(inviteCode, credentials)) self.config.set('Agent', 'Initialized', 'true') try: - self.MachineId = credentials#credentials['id'] - # self.username = credentials['mqtt']['username'] - # self.password = credentials['mqtt']['password'] - # self.clientId = credentials['mqtt']['clientId'] + self.MachineId = credentials #credentials['id'] + self.username = 'username' #credentials['mqtt']['username'] + self.password = 'password' #credentials['mqtt']['password'] + self.clientId = 'client_id' #credentials['mqtt']['clientId'] self.config.set('Agent', 'Id', self.MachineId) except: exception('Invalid credentials, closing the process') @@ -568,62 +510,31 @@ def CheckSubscription(self): @property def Start(self): - #debug('Start') - if self.connected: - ret = False - error('Start already connected') - else: - count = 0 - with self.mutex: - count+=1 - while self.connected == False and count < 30: - try: - self.sock = None - self.wrappedSocket = None - ##debug('Start wrap_socket') - self.sock = socket(AF_INET, SOCK_STREAM) - #self.wrappedSocket = wrap_socket(self.sock, ca_certs="/etc/myDevices/ca.crt", cert_reqs=CERT_REQUIRED) - self.wrappedSocket = wrap_socket(self.sock) - self.wrappedSocket.connect(('cloud.mydevices.com', 8181)) - info('myDevices cloud connected') - self.mqttClient = cayennemqtt.CayenneMQTTClient() - self.mqttClient.on_message = self.OnMessage - self.mqttClient.on_command = self.OnCommand - username = self.config.get('Agent', 'Username') - password = self.config.get('Agent', 'Password') - clientID = self.config.get('Agent', 'ClientID') - self.mqttClient.begin(username, password, clientID, self.HOST, self.PORT) - self.mqttClient.loop_start() - self.connected = True - except socket_error as serr: - Daemon.OnFailure('cloud', serr.errno) - error ('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(serr)) - self.connected = False - sleep(30-count) - return self.connected + """Connect to the server""" + started = False + count = 0 + while started == False and count < 30: + try: + self.mqttClient = cayennemqtt.CayenneMQTTClient() + self.mqttClient.on_message = self.OnMessage + self.mqttClient.begin(self.username, self.password, self.clientId, self.HOST, self.PORT) + self.mqttClient.loop_start() + started = True + except OSError as oserror: + Daemon.OnFailure('cloud', oserror.errno) + error ('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(oserror)) + started = False + sleep(30-count) + return started def Stop(self): """Disconnect from the server""" - #debug('Stop started') Daemon.Reset('cloud') - ret = True - if self.connected == False: - ret = False - error('Stop not connected') - else: - with self.mutex: - try: - self.wrappedSocket.shutdown(SHUT_RDWR) - self.wrappedSocket.close() - self.mqttClient.loop_stop() - info('myDevices cloud disconnected') - except socket_error as serr: - debug(str(serr)) - error ('myDevices cloud disconnected error:' + str(serr)) - ret = False - self.connected = False - #debug('Stop finished') - return ret + try: + self.mqttClient.loop_stop() + info('myDevices cloud disconnected') + except: + exception('Error stopping client') def Restart(self): """Restart the server connection""" @@ -685,87 +596,6 @@ def OnMessage(self, topic, message): info('OnMessage: {}'.format(message)) self.readQueue.put(message) - def OnCommand(self, channel, value): - """Handle command message from the server""" - info('OnCommand: channel {}, value {}'.format(channel, value)) - try: - channel = int(channel) - value = int(value) - except ValueError: - pass - retValue = str(self.sensorsClient.GpioCommand('value', 'POST', channel, value)) - if retValue == str(value): - return None - else: - return retValue - - def ReadMessage(self): - """Read a message from the server and add it to the queue""" - ret = True - if self.connected == False: - ret = False - else: - try: - self.count=4096 - timeout_in_seconds=10 - ready = select([self.wrappedSocket], [], [], timeout_in_seconds) - if ready[0]: - message = self.wrappedSocket.recv(self.count).decode() - buffering = len(message) == 4096 - while buffering and message: - if self.CheckJson(message): - buffering = False - else: - more = self.wrappedSocket.recv(self.count).decode() - if not more: - buffering = False - else: - message += more - try: - if message: - messageObject = loads(message) - # topic = self.topics.get(int(messageObject['PacketType'])) - topic = cayennemqtt.COMMAND_TOPIC #'{}/{}'.format(cayennemqtt.COMMAND_TOPIC, self.MachineId) - if messageObject: - info('ReadMessage, type: {}, topic: {}'.format(int(messageObject['PacketType']), topic)) - # if topic: - # #Debug hack for testing. This should be removed when completely switched over to MQTT. - # if topic is cayennemqtt.COMMAND_JSON_TOPIC: - # info('ReadMessage, Service: {}'.format(messageObject['Service'])) - # if messageObject['Service'] == 'gpio': - # info('ReadMessage, {}'.format(messageObject)) - # info('ReadMessage, check success: {}'.format(messageObject['Service'])) - # topic = 'cmd/{}'.format(messageObject['Parameters']['Channel']) - # message = '{},{}'.format(messageObject['Id'], messageObject['Parameters']['Value']) - # info('ReadMessage, topic: {}, message {}'.format(topic, message)) - # self.mqttClient.publish_packet(topic, message) - # elif topic is cayennemqtt.SETTINGS_SCHEDULE_JSON_TOPIC: - # self.mqttClient.publish_packet(topic, message, 1, True) - # elif topic is not cayennemqtt.SYSTEM_JSON_TOPIC and topic is not cayennemqtt.DATA_JSON_TOPIC: - # self.mqttClient.publish_packet(topic, message) - self.mqttClient.publish_packet(cayennemqtt.COMMAND_TOPIC, message) - else: - self.readQueue.put(messageObject) - del message - else: - error('ReadMessage received empty message string') - except: - exception('ReadMessage error: ' + str(message)) - return False - Daemon.Reset('cloud') - except IOError as ioerr: - debug('IOError: ' + str(ioerr)) - self.Restart() - #Daemon.OnFailure('cloud', ioerr.errno) - except socket_error as serr: - Daemon.OnFailure('cloud', serr.errno) - except: - exception('ReadMessage error') - ret = False - sleep(1) - Daemon.OnFailure('cloud') - return ret - def RunAction(self, action): """Run a specified action""" debug('RunAction') @@ -801,24 +631,8 @@ def ProcessMessage(self): return False except Empty: return False - with self.mutex: - retVal = self.CheckPT_ACK(messageObject) - if retVal: - return self.ExecuteMessage(messageObject) - def CheckPT_ACK(self, messageObject): - """Check if message is a keep alive packet""" - try: - packetType = int(messageObject['PacketType']) - if packetType == PacketTypes.PT_ACK.value: - self.lastPing = time() - return True - except: - debug('') - error('CheckPT_ACK failure: ' + str(messageObject)) - return False - def ExecuteMessage(self, messageObject): """Execute an action described in a message object""" if not messageObject: @@ -1109,31 +923,6 @@ def DequeuePacket(self): packet = None return packet - def CheckConnectionAndPing(self): - """Check that the server connection is still alive and send a keep alive packet at intervals""" - ticksStart = time() - with self.mutex: - try: - if ticksStart - self.lastPing > self.pingTimeout: - self.Stop() - self.Start - self.lastPing = time() - self.pingRate - 1 - warn('Restarting cloud connection -> CheckConnectionAndPing EXPIRED: ' + str(self.lastPing)) - if ticksStart - self.waitPing >= self.pingRate: - self.SendAckPacket() - except: - error('CheckConnectionAndPing error') - - def SendAckPacket(self): - """Enqueue a keep alive packet to send to the server""" - data = {} - debug('Last ping: ' + str(self.lastPing) + ' Wait ping: ' + str(self.waitPing)) - data['MachineName'] = self.MachineId - data['IPAddress'] = self.PublicIP - data['PacketType'] = PacketTypes.PT_ACK.value - self.EnqueuePacket(data) - self.waitPing = time() - def RequestSchedules(self): """Enqueue a packet to request schedules from the server""" data = {} diff --git a/myDevices/test/cayennemqtt_test.py b/myDevices/test/cayennemqtt_test.py index 6c4b521..32933c6 100644 --- a/myDevices/test/cayennemqtt_test.py +++ b/myDevices/test/cayennemqtt_test.py @@ -15,7 +15,6 @@ def setUp(self): # print('setUp') self.mqttClient = cayennemqtt.CayenneMQTTClient() self.mqttClient.on_message = self.OnMessage - self.mqttClient.on_command = self.OnCommand self.mqttClient.begin(TEST_USERNAME, TEST_PASSWORD, TEST_CLIENT_ID, TEST_HOST, TEST_PORT) self.mqttClient.loop_start() self.testClient = mqtt.Client("testID") @@ -37,10 +36,6 @@ def OnMessage(self, topic, message): self.receivedMessage = message # print('OnMessage: {} {}'.format(self.receivedTopic, self.receivedMessage)) - def OnCommand(self, channel, value): - # print('OnCommand: {} {}'.format(channel, value)) - return None - def OnTestMessage(self, client, userdata, message): self.receivedTopic = message.topic self.receivedMessage = message.payload.decode() From 00f62099f9ea456c9ea2825d73cefcd900e8e0ff Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 22 Sep 2017 17:49:20 -0600 Subject: [PATCH 054/129] Code cleanup. --- myDevices/cloud/client.py | 60 --------------------------------------- 1 file changed, 60 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index e53145a..6b61433 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -7,7 +7,6 @@ from socket import SOCK_STREAM, socket, AF_INET, gethostname, SHUT_RDWR from ssl import CERT_REQUIRED, wrap_socket from json import dumps, loads -from socket import error as socket_error from threading import Thread, RLock from time import strftime, localtime, tzset, time, sleep from queue import Queue, Empty @@ -305,8 +304,6 @@ def Destroy(self): self.updater.stop() if hasattr(self, 'writerThread'): self.writerThread.stop() - if hasattr(self, 'readerThread'): - self.readerThread.stop() if hasattr(self, 'processorThread'): self.processorThread.stop() ThreadPool.Shutdown() @@ -316,20 +313,6 @@ def Destroy(self): def FirstRun(self): """Send messages when client is first started""" self.SendSystemInfo() - # self.RequestSchedules() - # self.BuildPT_LOCATION() - # def BuildPT_LOCATION(self): - # data = {} - # data['position'] = {} - # data['position']['latitude'] = '30.022112' - # data['position']['longitude'] = '45.022112' - # data['position']['accuracy'] = '20' - # data['position']['method'] = 'Windows location provider' - # data['provider'] = 'other' - # data['time'] = int(time()) - # data['PacketType'] = PacketTypes.PT_LOCATION.value - # data['MachineName'] = self.MachineId - # self.EnqueuePacket(data) def OnDataChanged(self, raspberryValue): """Enqueue a packet containing changed system data to send to the server""" @@ -544,45 +527,6 @@ def Restart(self): self.Stop() self.Start - def SendMessage(self, message): - """Send a message packet to the server""" - logJson(message, 'SendMessage') - ret = True - if self.connected == False: - error('SendMessage fail') - ret = False - else: - try: - data = bytes(message, 'UTF-8') - max_size = 16383 - if len(data) > max_size: - start = 0 - current = max_size - end = len(data) - self.wrappedSocket.send(data[start:current]) - while current < end: - start = current - current = start + max_size if start + max_size < end else end - self.wrappedSocket.send(data[start:current]) - else: - self.wrappedSocket.send(data) - if self.onMessageSent: - self.onMessageSent(message) - message = None - except socket_error as serr: - error('SendMessage:' + str(serr)) - ret = False - Daemon.OnFailure('cloud', serr.errno) - sleep(1) - except IOError as ioerr: - debug('IOError: ' + str(ioerr)) - self.Restart() - except socket_error as serr: - Daemon.OnFailure('cloud', serr.errno) - except: - exception('SendMessage error') - return ret - def CheckJson(self, message): """Check if a JSON message is valid""" try: @@ -668,10 +612,6 @@ def ExecuteMessage(self, messageObject): self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) info(PacketTypes.PT_PRODUCT_INFO) return - # if packetType == PacketTypes.PT_START_RDS_LOCAL_INIT.value: - # error('PT_START_RDS_LOCAL_INIT not implemented') - # info(PacketTypes.PT_START_RDS_LOCAL_INIT) - # return if packetType == PacketTypes.PT_RESTART_COMPUTER.value: info(PacketTypes.PT_RESTART_COMPUTER) data = {} From f6c16a05a90e50a18df38086d02cf0d73be7cfa5 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 4 Oct 2017 16:48:29 -0600 Subject: [PATCH 055/129] Remove unused packet type. --- myDevices/cloud/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 6b61433..5d0df0e 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -37,7 +37,6 @@ @unique class PacketTypes(Enum): """Packet types used when sending/receiving messages""" - PT_ACK = 0 PT_UTILIZATION = 3 PT_SYSTEM_INFO = 4 PT_PROCESS_LIST = 5 From c2041ffa9098def7a51925b9db328e278b01a6ae Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 5 Oct 2017 17:16:41 -0600 Subject: [PATCH 056/129] Update sensor info sent to server. --- myDevices/devices/manager.py | 3 +-- myDevices/sensors/sensors.py | 36 ++++++++++++++++------------------ myDevices/test/sensors_test.py | 23 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/myDevices/devices/manager.py b/myDevices/devices/manager.py index a37e4c1..28f03fc 100644 --- a/myDevices/devices/manager.py +++ b/myDevices/devices/manager.py @@ -28,8 +28,7 @@ def deviceDetector(): saveDevice(dev['name'], int(time())) except Exception as e: logger.error("Device detector: %s" % e) - - sleep(5) + # sleep(5) def findDeviceClass(name): for package in PACKAGES: diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 95a2cea..dcc3ca0 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -23,8 +23,8 @@ from myDevices.utils.types import M_JSON from myDevices.system.systeminfo import SystemInfo -REFRESH_FREQUENCY = 0.10 #seconds -SENSOR_INFO_SLEEP = 0.05 +REFRESH_FREQUENCY = 5 #seconds +# SENSOR_INFO_SLEEP = 0.05 class SensorsClient(): """Class for interfacing with sensors and actuators""" @@ -116,10 +116,8 @@ def MonitorSensors(self): if self.previousSensorsInfo: del self.previousSensorsInfo self.previousSensorsInfo = None - if mergedSensors is None: - self.previousSensorsInfo = self.currentSensorsInfo - return - self.raspberryValue['SensorsInfo'] = mergedSensors + if mergedSensors: + self.raspberryValue['SensorsInfo'] = mergedSensors self.previousSensorsInfo = self.currentSensorsInfo debug(str(time()) + ' Merge sensors info ' + str(self.sensorsRefreshCount)) @@ -290,12 +288,12 @@ def SensorsInfo(self): for value in devices: sensor = instance.deviceInstance(value['name']) if 'enabled' not in value or value['enabled'] == 1: - sleep(SENSOR_INFO_SLEEP) + # sleep(SENSOR_INFO_SLEEP) try: if value['type'] == 'Temperature': value['Celsius'] = self.CallDeviceFunction(sensor.getCelsius) - value['Fahrenheit'] = self.CallDeviceFunction(sensor.getFahrenheit) - value['Kelvin'] = self.CallDeviceFunction(sensor.getKelvin) + # value['Fahrenheit'] = self.CallDeviceFunction(sensor.getFahrenheit) + # value['Kelvin'] = self.CallDeviceFunction(sensor.getKelvin) if value['type'] == 'Pressure': value['Pascal'] = self.CallDeviceFunction(sensor.getPascal) if value['type'] == 'Luminosity': @@ -305,17 +303,17 @@ def SensorsInfo(self): value['Inch'] = self.CallDeviceFunction(sensor.getInch) if value['type'] in ('ADC', 'DAC'): value['channelCount'] = self.CallDeviceFunction(sensor.analogCount) - value['maxInteger'] = self.CallDeviceFunction(sensor.analogMaximum) - value['resolution'] = self.CallDeviceFunction(sensor.analogResolution) - value['allInteger'] = self.CallDeviceFunction(sensor.analogReadAll) - value['allVolt'] = self.CallDeviceFunction(sensor.analogReadAllVolt) + # value['maxInteger'] = self.CallDeviceFunction(sensor.analogMaximum) + # value['resolution'] = self.CallDeviceFunction(sensor.analogResolution) + # value['allInteger'] = self.CallDeviceFunction(sensor.analogReadAll) + # value['allVolt'] = self.CallDeviceFunction(sensor.analogReadAllVolt) value['allFloat'] = self.CallDeviceFunction(sensor.analogReadAllFloat) - if value['type'] == 'DAC': - value['vref'] = self.CallDeviceFunction(sensor.analogReference) + # if value['type'] == 'DAC': + # value['vref'] = self.CallDeviceFunction(sensor.analogReference) if value['type'] == 'PWM': value['channelCount'] = self.CallDeviceFunction(sensor.pwmCount) - value['maxInteger'] = self.CallDeviceFunction(sensor.pwmMaximum) - value['resolution'] = self.CallDeviceFunction(sensor.pwmResolution) + # value['maxInteger'] = self.CallDeviceFunction(sensor.pwmMaximum) + # value['resolution'] = self.CallDeviceFunction(sensor.pwmResolution) value['all'] = self.CallDeviceFunction(sensor.pwmWildcard) if value['type'] == 'Humidity': value['float'] = self.CallDeviceFunction(sensor.getHumidity) @@ -326,9 +324,9 @@ def SensorsInfo(self): value['channelCount'] = self.CallDeviceFunction(sensor.digitalCount) value['all'] = self.CallDeviceFunction(sensor.wildcard) if value['type'] == 'AnalogSensor': - value['integer'] = self.CallDeviceFunction(sensor.read) + # value['integer'] = self.CallDeviceFunction(sensor.read) value['float'] = self.CallDeviceFunction(sensor.readFloat) - value['volt'] = self.CallDeviceFunction(sensor.readVolt) + # value['volt'] = self.CallDeviceFunction(sensor.readVolt) if value['type'] == 'ServoMotor': value['angle'] = self.CallDeviceFunction(sensor.readAngle) if value['type'] == 'AnalogActuator': diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index aa0510b..0a6a633 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -23,6 +23,29 @@ def tearDownClass(cls): del cls.client del GPIO.instance + def OnDataChanged(self, sensor_data): + try: + self.previousBusInfo = self.currentBusInfo + self.currentBusInfo = sensor_data['BusInfo'] + self.previousSensorsInfo = self.currentSensorsInfo + self.currentSensorsInfo = sensor_data['SensorsInfo'] + if self.previousBusInfo and self.previousSensorsInfo: + self.done = True + except: + pass + + def testSensorMonitor(self): + self.previousBusInfo = None + self.currentBusInfo = None + self.previousSensorsInfo = None + self.currentSensorsInfo = None + self.done = False + SensorsClientTest.client.SetDataChanged(self.OnDataChanged) + while not self.done: + sleep(1) + self.assertNotEqual(self.previousBusInfo, self.currentBusInfo) + self.assertNotEqual(self.previousSensorsInfo, self.currentSensorsInfo) + def testBusInfo(self): bus = SensorsClientTest.client.BusInfo() # # Compare our GPIO function values with the ones from RPi.GPIO library From 3dffb91e4f215a26b472dce1a9731cd1292923f4 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 6 Oct 2017 11:21:51 -0600 Subject: [PATCH 057/129] Change raspberryValue to be more descriptive. --- myDevices/cloud/client.py | 92 ++++++++++++++++++------------------ myDevices/sensors/sensors.py | 21 ++++---- 2 files changed, 56 insertions(+), 57 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 70f3252..f2e70a8 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -314,16 +314,16 @@ def FirstRun(self): """Send messages when client is first started""" self.SendSystemInfo() - def OnDataChanged(self, raspberryValue): + def OnDataChanged(self, systemData): """Enqueue a packet containing changed system data to send to the server""" data = {} data['MachineName'] = self.MachineId data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value data['Timestamp'] = int(time()) - data['RaspberryInfo'] = raspberryValue + data['RaspberryInfo'] = systemData self.EnqueuePacket(data) del data - del raspberryValue + del systemData def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" @@ -334,30 +334,30 @@ def SendSystemInfo(self): data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value data['IpAddress'] = self.PublicIP data['GatewayMACAddress'] = self.hardware.getMac() - raspberryValue = {} - # raspberryValue['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - raspberryValue['AntiVirus'] = 'None' - raspberryValue['Firewall'] = 'iptables' - raspberryValue['FirewallEnabled'] = 'true' - raspberryValue['ComputerMake'] = self.hardware.getManufacturer() - raspberryValue['ComputerModel'] = self.hardware.getModel() - raspberryValue['OsName'] = self.oSInfo.ID - raspberryValue['OsBuild'] = self.oSInfo.ID_LIKE - raspberryValue['OsArchitecture'] = self.hardware.Revision - raspberryValue['OsVersion'] = self.oSInfo.VERSION_ID - raspberryValue['ComputerName'] = self.machineName - raspberryValue['AgentVersion'] = self.config.get('Agent','Version') - raspberryValue['GatewayMACAddress'] = self.hardware.getMac() - raspberryValue['OsSettings'] = SystemConfig.getConfig() - raspberryValue['NetworkId'] = WifiManager.Network.GetNetworkId() - raspberryValue['WifiStatus'] = self.wifiManager.GetStatus() - data['RaspberryInfo'] = raspberryValue + systemData = {} + # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) + systemData['AntiVirus'] = 'None' + systemData['Firewall'] = 'iptables' + systemData['FirewallEnabled'] = 'true' + systemData['ComputerMake'] = self.hardware.getManufacturer() + systemData['ComputerModel'] = self.hardware.getModel() + systemData['OsName'] = self.oSInfo.ID + systemData['OsBuild'] = self.oSInfo.ID_LIKE + systemData['OsArchitecture'] = self.hardware.Revision + systemData['OsVersion'] = self.oSInfo.VERSION_ID + systemData['ComputerName'] = self.machineName + systemData['AgentVersion'] = self.config.get('Agent','Version') + systemData['GatewayMACAddress'] = self.hardware.getMac() + systemData['OsSettings'] = SystemConfig.getConfig() + systemData['NetworkId'] = WifiManager.Network.GetNetworkId() + systemData['WifiStatus'] = self.wifiManager.GetStatus() + data['RaspberryInfo'] = systemData if data != self.previousSystemInfo: self.previousSystemInfo = data.copy() data['Timestamp'] = int(time()) self.EnqueuePacket(data) logJson('SendSystemInfo: ' + dumps(data), 'SendSystemInfo') - del raspberryValue + del systemData del data data=None except Exception as e: @@ -391,37 +391,37 @@ def SendSystemState(self): data['Timestamp'] = int(time()) data['IpAddress'] = self.PublicIP data['GatewayMACAddress'] = self.hardware.getMac() - raspberryValue = {} - raspberryValue['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - raspberryValue['AntiVirus'] = 'None' - raspberryValue['Firewall'] = 'iptables' - raspberryValue['FirewallEnabled'] = 'true' - raspberryValue['ComputerMake'] = self.hardware.getManufacturer() - raspberryValue['ComputerModel'] = self.hardware.getModel() - raspberryValue['OsName'] = self.oSInfo.ID - raspberryValue['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID - raspberryValue['OsArchitecture'] = self.hardware.Revision - raspberryValue['OsVersion'] = self.oSInfo.VERSION_ID - raspberryValue['ComputerName'] = self.machineName - raspberryValue['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') - raspberryValue['InstallDate'] = self.installDate - raspberryValue['GatewayMACAddress'] = self.hardware.getMac() + systemData = {} + systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) + systemData['AntiVirus'] = 'None' + systemData['Firewall'] = 'iptables' + systemData['FirewallEnabled'] = 'true' + systemData['ComputerMake'] = self.hardware.getManufacturer() + systemData['ComputerModel'] = self.hardware.getModel() + systemData['OsName'] = self.oSInfo.ID + systemData['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID + systemData['OsArchitecture'] = self.hardware.Revision + systemData['OsVersion'] = self.oSInfo.VERSION_ID + systemData['ComputerName'] = self.machineName + systemData['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') + systemData['InstallDate'] = self.installDate + systemData['GatewayMACAddress'] = self.hardware.getMac() with self.sensorsClient.sensorMutex: - raspberryValue['SystemInfo'] = self.sensorsClient.currentSystemInfo - raspberryValue['SensorsInfo'] = self.sensorsClient.currentSensorsInfo - raspberryValue['BusInfo'] = self.sensorsClient.currentBusInfo - raspberryValue['OsSettings'] = SystemConfig.getConfig() - raspberryValue['NetworkId'] = WifiManager.Network.GetNetworkId() - raspberryValue['WifiStatus'] = self.wifiManager.GetStatus() + systemData['SystemInfo'] = self.sensorsClient.currentSystemInfo + systemData['SensorsInfo'] = self.sensorsClient.currentSensorsInfo + systemData['BusInfo'] = self.sensorsClient.currentBusInfo + systemData['OsSettings'] = SystemConfig.getConfig() + systemData['NetworkId'] = WifiManager.Network.GetNetworkId() + systemData['WifiStatus'] = self.wifiManager.GetStatus() try: history = History() - history.SaveAverages(raspberryValue) + history.SaveAverages(systemData) except: exception('History error') - data['RaspberryInfo'] = raspberryValue + data['RaspberryInfo'] = systemData self.EnqueuePacket(data) logJson('PT_SYSTEM_INFO: ' + dumps(data), 'PT_SYSTEM_INFO') - del raspberryValue + del systemData del data data = None except Exception as e: diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index f8e6d79..2d9d5c6 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -80,8 +80,7 @@ def Monitor(self): while self.continueMonitoring: try: if datetime.now() > nextTime: - self.raspberryValue = None - self.raspberryValue = {} + self.systemData = {} refreshTime = int(time()) if datetime.now() > nextTimeSystemInfo: with self.systemMutex: @@ -90,8 +89,8 @@ def Monitor(self): nextTimeSystemInfo = datetime.now() + timedelta(seconds=5) self.MonitorSensors() self.MonitorBus() - if self.onDataChanged and self.raspberryValue: - self.onDataChanged(self.raspberryValue) + if self.onDataChanged and self.systemData: + self.onDataChanged(self.systemData) bResult = self.RemoveRefresh(refreshTime) if bResult and self.onSystemInfo: self.onSystemInfo() @@ -116,7 +115,7 @@ def MonitorSensors(self): del self.previousSensorsInfo self.previousSensorsInfo = None if mergedSensors: - self.raspberryValue['SensorsInfo'] = mergedSensors + self.systemData['SensorsInfo'] = mergedSensors self.previousSensorsInfo = self.currentSensorsInfo debug(str(time()) + ' Merge sensors info ' + str(self.sensorsRefreshCount)) @@ -148,7 +147,7 @@ def MonitorBus(self): self.BusInfo() debug(str(time()) + ' Got bus info ' + str(self.sensorsRefreshCount)) if self.SHA_Calc(self.currentBusInfo) != self.SHA_Calc(self.previousBusInfo): - self.raspberryValue['BusInfo'] = self.currentBusInfo + self.systemData['BusInfo'] = self.currentBusInfo if self.previousBusInfo: del self.previousBusInfo self.previousBusInfo = None @@ -168,7 +167,7 @@ def MonitorSystemInformation(self): del self.previousSystemInfo self.previousSystemInfo = None self.previousSystemInfo = self.currentSystemInfo - self.raspberryValue['SystemInfo'] = self.currentSystemInfo + self.systemData['SystemInfo'] = self.currentSystemInfo def SystemInformation(self): """Return dict containing current system info, including CPU, RAM, storage and network info""" @@ -410,13 +409,13 @@ def EditSensor(self, name, description, device, args): currentSensorsDictionary = dict((i['sensor'], i) for i in self.currentSensorsInfo) sensorData = currentSensorsDictionary[hashKey] sensor = sensorData[hashKey] - raspberryValue = {} + systemData = {} sensor['args'] = args sensor['description'] = description - raspberryValue['SensorsInfo'] = [] - raspberryValue['SensorsInfo'].append(sensor) + systemData['SensorsInfo'] = [] + systemData['SensorsInfo'].append(sensor) if self.onDataChanged: - self.onDataChanged(raspberryValue) + self.onDataChanged(systemData) except: pass if retValue[0] == 200: From 2f8ccf3bb736886b3fedf0e7b07e0869f8149ee6 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 6 Oct 2017 18:01:10 -0600 Subject: [PATCH 058/129] Send changed system info only. --- myDevices/sensors/sensors.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 2d9d5c6..d6dbb8b 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -160,14 +160,16 @@ def MonitorSystemInformation(self): debug(str(time()) + ' Get system info ' + str(self.sensorsRefreshCount)) self.SystemInformation() debug(str(time()) + ' Got system info ' + str(self.sensorsRefreshCount)) - firstSHA = self.SHA_Calc(self.currentSystemInfo) - secondSHA = self.SHA_Calc(self.previousSystemInfo) - if firstSHA != secondSHA: - if self.previousSystemInfo: - del self.previousSystemInfo - self.previousSystemInfo = None + if self.currentSystemInfo != self.previousSystemInfo: + changedSystemInfo = {} + for key in self.currentSystemInfo.keys(): + if self.previousSystemInfo and key in self.previousSystemInfo: + if self.currentSystemInfo[key] != self.previousSystemInfo[key]: + changedSystemInfo[key] = self.currentSystemInfo[key] + else: + changedSystemInfo[key] = self.currentSystemInfo[key] + self.systemData['SystemInfo'] = changedSystemInfo self.previousSystemInfo = self.currentSystemInfo - self.systemData['SystemInfo'] = self.currentSystemInfo def SystemInformation(self): """Return dict containing current system info, including CPU, RAM, storage and network info""" From 564212a414a29888a696af0e36c48f2d289515e2 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 6 Oct 2017 18:03:44 -0600 Subject: [PATCH 059/129] Test system info monitoring. --- myDevices/test/sensors_test.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index 3e874cc..b1d50a5 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -24,25 +24,34 @@ def tearDownClass(cls): del cls.client def OnDataChanged(self, sensor_data): - try: + if 'BusInfo' in sensor_data: self.previousBusInfo = self.currentBusInfo self.currentBusInfo = sensor_data['BusInfo'] + if 'SensorsInfo' in sensor_data: self.previousSensorsInfo = self.currentSensorsInfo self.currentSensorsInfo = sensor_data['SensorsInfo'] - if self.previousBusInfo and self.previousSensorsInfo: - self.done = True - except: - pass + if 'SystemInfo' in sensor_data: + self.previousSystemInfo = self.currentSystemInfo + self.currentSystemInfo = sensor_data['SystemInfo'] + if self.previousBusInfo and self.previousSensorsInfo and self.previousSystemInfo: + self.done = True - def testSensorMonitor(self): + def testMonitor(self): self.previousBusInfo = None self.currentBusInfo = None self.previousSensorsInfo = None self.currentSensorsInfo = None + self.previousSystemInfo = None + self.currentSystemInfo = None self.done = False SensorsClientTest.client.SetDataChanged(self.OnDataChanged) - while not self.done: - sleep(1) + self.setChannelFunction(GPIO().pins[7], 'OUT') + for i in range(5): + sleep(5) + self.setChannelValue(GPIO().pins[7], i % 2) + if self.done: + break + self.assertNotEqual(self.previousSystemInfo, self.currentSystemInfo) self.assertNotEqual(self.previousBusInfo, self.currentBusInfo) self.assertNotEqual(self.previousSensorsInfo, self.currentSensorsInfo) From 763349d97b115032121475058aebfa33fa6690c3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 6 Oct 2017 18:06:50 -0600 Subject: [PATCH 060/129] Comment out unused sensor data from historical averages. --- myDevices/utils/history.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/myDevices/utils/history.py b/myDevices/utils/history.py index 73b4cb7..30f9b4b 100644 --- a/myDevices/utils/history.py +++ b/myDevices/utils/history.py @@ -192,8 +192,8 @@ def CalculateSensorsInfoAverages(self, current_avgs, new_sample, count_sensor): count_sensor['SensorsInfo'][value['sensor']] += 1 if value['type'] == 'Temperature': averageSensorsDictionary[value['sensor']]['Celsius'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['Celsius'], value['Celsius'], count_sensor['SensorsInfo'][value['sensor']] ) - averageSensorsDictionary[value['sensor']]['Fahrenheit'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['Fahrenheit'], value['Fahrenheit'], count_sensor['SensorsInfo'][value['sensor']] ) - averageSensorsDictionary[value['sensor']]['Kelvin'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['Kelvin'], value['Kelvin'], count_sensor['SensorsInfo'][value['sensor']] ) + # averageSensorsDictionary[value['sensor']]['Fahrenheit'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['Fahrenheit'], value['Fahrenheit'], count_sensor['SensorsInfo'][value['sensor']] ) + # averageSensorsDictionary[value['sensor']]['Kelvin'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['Kelvin'], value['Kelvin'], count_sensor['SensorsInfo'][value['sensor']] ) if value['type'] == 'Pressure': averageSensorsDictionary[value['sensor']]['Pascal'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['Pascal'], value['Pascal'], count_sensor['SensorsInfo'][value['sensor']] ) if value['type'] == 'Luminosity': @@ -224,8 +224,8 @@ def CalculateSensorsInfoAverages(self, current_avgs, new_sample, count_sensor): averageSensorsDictionary[value['sensor']]['value'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['value'], value['value'], count_sensor['SensorsInfo'][value['sensor']] ) if value['type'] == 'AnalogSensor': averageSensorsDictionary[value['sensor']]['float'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['float'], value['float'], count_sensor['SensorsInfo'][value['sensor']] ) - averageSensorsDictionary[value['sensor']]['integer'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['integer'], value['integer'], count_sensor['SensorsInfo'][value['sensor']] ) - averageSensorsDictionary[value['sensor']]['volt'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['volt'], value['volt'], count_sensor['SensorsInfo'][value['sensor']] ) + # averageSensorsDictionary[value['sensor']]['integer'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['integer'], value['integer'], count_sensor['SensorsInfo'][value['sensor']] ) + # averageSensorsDictionary[value['sensor']]['volt'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['volt'], value['volt'], count_sensor['SensorsInfo'][value['sensor']] ) if value['type'] == 'ServoMotor': averageSensorsDictionary[value['sensor']]['angle'] = self.CalculateAverage(averageSensorsDictionary[value['sensor']]['angle'], value['angle'], count_sensor['SensorsInfo'][value['sensor']] ) if value['type'] == 'AnalogActuator': From fe0b4885c909752d79c6e0213d8ac1fb23574a61 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 9 Oct 2017 12:25:30 -0600 Subject: [PATCH 061/129] Comment out unneeded functionality. --- myDevices/cloud/client.py | 136 +++++++++++++++---------------- myDevices/system/systemconfig.py | 26 +++--- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index f2e70a8..b614e82 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -40,17 +40,17 @@ class PacketTypes(Enum): """Packet types used when sending/receiving messages""" PT_UTILIZATION = 3 PT_SYSTEM_INFO = 4 - PT_PROCESS_LIST = 5 - PT_STARTUP_APPLICATIONS = 8 + # PT_PROCESS_LIST = 5 + # PT_STARTUP_APPLICATIONS = 8 PT_START_RDS = 11 PT_STOP_RDS = 12 PT_RESTART_COMPUTER = 25 PT_SHUTDOWN_COMPUTER = 26 - PT_KILL_PROCESS = 27 + # PT_KILL_PROCESS = 27 PT_REQUEST_SCHEDULES = 40 PT_UPDATE_SCHEDULES = 41 PT_AGENT_MESSAGE = 45 - PT_PRODUCT_INFO = 50 + # PT_PRODUCT_INFO = 50 PT_UNINSTALL_AGENT = 51 PT_ADD_SENSOR = 61 PT_REMOVE_SENSOR = 62 @@ -336,14 +336,14 @@ def SendSystemInfo(self): data['GatewayMACAddress'] = self.hardware.getMac() systemData = {} # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - systemData['AntiVirus'] = 'None' - systemData['Firewall'] = 'iptables' - systemData['FirewallEnabled'] = 'true' + # systemData['AntiVirus'] = 'None' + # systemData['Firewall'] = 'iptables' + # systemData['FirewallEnabled'] = 'true' systemData['ComputerMake'] = self.hardware.getManufacturer() systemData['ComputerModel'] = self.hardware.getModel() systemData['OsName'] = self.oSInfo.ID - systemData['OsBuild'] = self.oSInfo.ID_LIKE - systemData['OsArchitecture'] = self.hardware.Revision + # systemData['OsBuild'] = self.oSInfo.ID_LIKE + # systemData['OsArchitecture'] = self.hardware.Revision systemData['OsVersion'] = self.oSInfo.VERSION_ID systemData['ComputerName'] = self.machineName systemData['AgentVersion'] = self.config.get('Agent','Version') @@ -399,8 +399,8 @@ def SendSystemState(self): systemData['ComputerMake'] = self.hardware.getManufacturer() systemData['ComputerModel'] = self.hardware.getModel() systemData['OsName'] = self.oSInfo.ID - systemData['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID - systemData['OsArchitecture'] = self.hardware.Revision + # systemData['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID + # systemData['OsArchitecture'] = self.hardware.Revision systemData['OsVersion'] = self.oSInfo.VERSION_ID systemData['ComputerName'] = self.machineName systemData['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') @@ -427,47 +427,47 @@ def SendSystemState(self): except Exception as e: exception('ThreadSystemInfo unexpected error: ' + str(e)) - def BuildPT_STARTUP_APPLICATIONS(self): - """Schedule a function to run for retrieving a list of services""" - ThreadPool.Submit(self.ThreadServiceManager) - - def ThreadServiceManager(self): - """Enqueue a packet containing a list of services to send to the server""" - self.serviceManager.Run() - sleep(GENERAL_SLEEP_THREAD) - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_STARTUP_APPLICATIONS.value - data['ProcessList'] = self.serviceManager.GetServiceList() - self.EnqueuePacket(data) - - def BuildPT_PROCESS_LIST(self): - """Schedule a function to run for retrieving a list of processes""" - ThreadPool.Submit(self.ThreadProcessManager) - - def ThreadProcessManager(self): - """Enqueue a packet containing a list of processes to send to the server""" - self.processManager.Run() - sleep(GENERAL_SLEEP_THREAD) - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_PROCESS_LIST.value - data['ProcessList'] = self.processManager.GetProcessList() - self.EnqueuePacket(data) - - def ProcessPT_KILL_PROCESS(self, message): - """Kill a process specified in message""" - pid = message['Pid'] - retVal = self.processManager.KillProcess(int(pid)) - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value - data['Type'] = 'Info' - if retVal: - data['Message'] = 'Process Killed!' - else: - data['Message'] = 'Process not Killed!' - self.EnqueuePacket(data) + # def BuildPT_STARTUP_APPLICATIONS(self): + # """Schedule a function to run for retrieving a list of services""" + # ThreadPool.Submit(self.ThreadServiceManager) + + # def ThreadServiceManager(self): + # """Enqueue a packet containing a list of services to send to the server""" + # self.serviceManager.Run() + # sleep(GENERAL_SLEEP_THREAD) + # data = {} + # data['MachineName'] = self.MachineId + # data['PacketType'] = PacketTypes.PT_STARTUP_APPLICATIONS.value + # data['ProcessList'] = self.serviceManager.GetServiceList() + # self.EnqueuePacket(data) + + # def BuildPT_PROCESS_LIST(self): + # """Schedule a function to run for retrieving a list of processes""" + # ThreadPool.Submit(self.ThreadProcessManager) + + # def ThreadProcessManager(self): + # """Enqueue a packet containing a list of processes to send to the server""" + # self.processManager.Run() + # sleep(GENERAL_SLEEP_THREAD) + # data = {} + # data['MachineName'] = self.MachineId + # data['PacketType'] = PacketTypes.PT_PROCESS_LIST.value + # data['ProcessList'] = self.processManager.GetProcessList() + # self.EnqueuePacket(data) + + # def ProcessPT_KILL_PROCESS(self, message): + # """Kill a process specified in message""" + # pid = message['Pid'] + # retVal = self.processManager.KillProcess(int(pid)) + # data = {} + # data['MachineName'] = self.MachineId + # data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value + # data['Type'] = 'Info' + # if retVal: + # data['Message'] = 'Process Killed!' + # else: + # data['Message'] = 'Process not Killed!' + # self.EnqueuePacket(data) def CheckSubscription(self): """Check that an invite code is valid""" @@ -596,22 +596,22 @@ def ExecuteMessage(self, messageObject): command = "sudo /etc/myDevices/uninstall/uninstall.sh" executeCommand(command) return - if packetType == PacketTypes.PT_STARTUP_APPLICATIONS.value: - self.BuildPT_STARTUP_APPLICATIONS() - info(PacketTypes.PT_STARTUP_APPLICATIONS) - return - if packetType == PacketTypes.PT_PROCESS_LIST.value: - self.BuildPT_PROCESS_LIST() - info(PacketTypes.PT_PROCESS_LIST) - return - if packetType == PacketTypes.PT_KILL_PROCESS.value: - self.ProcessPT_KILL_PROCESS(messageObject) - info(PacketTypes.PT_KILL_PROCESS) - return - if packetType == PacketTypes.PT_PRODUCT_INFO.value: - self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) - info(PacketTypes.PT_PRODUCT_INFO) - return + # if packetType == PacketTypes.PT_STARTUP_APPLICATIONS.value: + # self.BuildPT_STARTUP_APPLICATIONS() + # info(PacketTypes.PT_STARTUP_APPLICATIONS) + # return + # if packetType == PacketTypes.PT_PROCESS_LIST.value: + # self.BuildPT_PROCESS_LIST() + # info(PacketTypes.PT_PROCESS_LIST) + # return + # if packetType == PacketTypes.PT_KILL_PROCESS.value: + # self.ProcessPT_KILL_PROCESS(messageObject) + # info(PacketTypes.PT_KILL_PROCESS) + # return + # if packetType == PacketTypes.PT_PRODUCT_INFO.value: + # self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) + # info(PacketTypes.PT_PRODUCT_INFO) + # return if packetType == PacketTypes.PT_RESTART_COMPUTER.value: info(PacketTypes.PT_RESTART_COMPUTER) data = {} diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index a080200..de99cf9 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -55,18 +55,18 @@ def getConfig(): configItem = {} if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): return configItem - try: - (returnCode, output) = SystemConfig.ExecuteConfigCommand(17, '') - if output: - values = output.strip().split(' ') - configItem['Camera'] = {} - for i in values: - if '=' in i: - val1 = i.split('=') - configItem['Camera'][val1[0]] = int(val1[1]) - del output - except: - exception('Camera config') + # try: + # (returnCode, output) = SystemConfig.ExecuteConfigCommand(17, '') + # if output: + # values = output.strip().split(' ') + # configItem['Camera'] = {} + # for i in values: + # if '=' in i: + # val1 = i.split('=') + # configItem['Camera'][val1[0]] = int(val1[1]) + # del output + # except: + # exception('Get camera config') try: (returnCode, output) = SystemConfig.ExecuteConfigCommand(10, '') @@ -82,6 +82,6 @@ def getConfig(): configItem['OneWire'] = int(output.strip()) del output except: - exception('Camera config') + exception('Get config') info('SystemConfig: {}'.format(configItem)) return configItem \ No newline at end of file From 134f16b6677a42620755adc272c5d94f592ed98b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 9 Oct 2017 16:43:42 -0600 Subject: [PATCH 062/129] Send static system info using new data channels. --- myDevices/cloud/cayennemqtt.py | 17 +++- myDevices/cloud/client.py | 140 ++++++++++++++++++--------------- myDevices/wifi/WifiManager.py | 46 +++++------ 3 files changed, 114 insertions(+), 89 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 7bfebfb..50be0c9 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -5,13 +5,22 @@ from myDevices.utils.logger import debug, error, exception, info, logJson, warn # Topics -DATA_TOPIC = "data" -COMMAND_TOPIC = "cmd" +DATA_TOPIC = 'data.json' +COMMAND_TOPIC = 'cmd' + +# Data Channels +SYS_HARDWARE_MAKE = 'sys:hw:make' +SYS_HARDWARE_MODEL = 'sys:hw:model' +SYS_OS_NAME = 'sys:os:name' +SYS_OS_VERSION = 'sys:os:version' +SYS_AGENT_VERSION = 'sys:agent:version' +SYS_ETHERNET_ADDRESS = 'sys:eth:{};address' +SYS_ETHERNET_SPEED = 'sys:eth:{};speed' class CayenneMQTTClient: """Cayenne MQTT Client class. - This is the main client class for connecting to Cayenne and sending and receiving data. + This is the main client class for connecting to Cayenne and sending and recFUeiving data. Standard usage: * Set on_message callback, if you are receiving data. @@ -38,7 +47,7 @@ def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', por hostname is the MQTT broker hostname. port is the MQTT broker port. """ - self.rootTopic = "v0.9/%s/things/%s" % (username, clientid) + self.rootTopic = "v2/things/%s" % clientid self.client = mqtt.Client(client_id=clientid, clean_session=True, userdata=self) self.client.on_connect = self.connect_callback self.client.on_disconnect = self.disconnect_callback diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index b614e82..c268304 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -38,7 +38,7 @@ @unique class PacketTypes(Enum): """Packet types used when sending/receiving messages""" - PT_UTILIZATION = 3 + # PT_UTILIZATION = 3 PT_SYSTEM_INFO = 4 # PT_PROCESS_LIST = 5 # PT_STARTUP_APPLICATIONS = 8 @@ -249,7 +249,7 @@ def __init__(self, host, port, cayenneApiHost): #self.defaultRDServer = self.networkConfig.get('CONFIG','RemoteDesktopServerAddress') self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.Initialize() - self.FirstRun() + # self.FirstRun() self.updater = Updater(self.config) self.updater.start() self.initialized = True @@ -283,6 +283,7 @@ def Initialize(self): self.writerThread.start() self.processorThread = ProcessorThread('processor', self) self.processorThread.start() + TimerThread(self.SendSystemInfo, 300) TimerThread(self.SendSystemState, 30, 5) self.previousSystemInfo = None self.sentHistoryData = {} @@ -310,9 +311,9 @@ def Destroy(self): self.Stop() info('Client shut down') - def FirstRun(self): - """Send messages when client is first started""" - self.SendSystemInfo() + # def FirstRun(self): + # """Send messages when client is first started""" + # self.SendSystemInfo() def OnDataChanged(self, systemData): """Enqueue a packet containing changed system data to send to the server""" @@ -325,66 +326,80 @@ def OnDataChanged(self, systemData): del data del systemData + def AppendDataChannel(self, data_list, channel, value): + """Create data dict and append it to a list""" + data = {} + data['channel'] = channel + data['value'] = value + data_list.append(data) + def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" try: # debug('SendSystemInfo') - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value - data['IpAddress'] = self.PublicIP - data['GatewayMACAddress'] = self.hardware.getMac() - systemData = {} - # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - # systemData['AntiVirus'] = 'None' - # systemData['Firewall'] = 'iptables' - # systemData['FirewallEnabled'] = 'true' - systemData['ComputerMake'] = self.hardware.getManufacturer() - systemData['ComputerModel'] = self.hardware.getModel() - systemData['OsName'] = self.oSInfo.ID - # systemData['OsBuild'] = self.oSInfo.ID_LIKE - # systemData['OsArchitecture'] = self.hardware.Revision - systemData['OsVersion'] = self.oSInfo.VERSION_ID - systemData['ComputerName'] = self.machineName - systemData['AgentVersion'] = self.config.get('Agent','Version') - systemData['GatewayMACAddress'] = self.hardware.getMac() - systemData['OsSettings'] = SystemConfig.getConfig() - systemData['NetworkId'] = WifiManager.Network.GetNetworkId() - systemData['WifiStatus'] = self.wifiManager.GetStatus() - data['RaspberryInfo'] = systemData - if data != self.previousSystemInfo: - self.previousSystemInfo = data.copy() - data['Timestamp'] = int(time()) - self.EnqueuePacket(data) - logJson('SendSystemInfo: ' + dumps(data), 'SendSystemInfo') - del systemData - del data - data=None - except Exception as e: - exception('SendSystemInfo unexpected error: ' + str(e)) - - def SendSystemUtilization(self): - """Enqueue a packet containing system utilization data to send to the server""" - data = {} - data['MachineName'] = self.MachineId - data['Timestamp'] = int(time()) - data['PacketType'] = PacketTypes.PT_UTILIZATION.value - self.processManager.RefreshProcessManager() - data['VisibleMemory'] = self.processManager.VisibleMemory - data['AvailableMemory'] = self.processManager.AvailableMemory - data['AverageProcessorUsage'] = self.processManager.AverageProcessorUsage - data['PeakProcessorUsage'] = self.processManager.PeakProcessorUsage - data['AverageMemoryUsage'] = self.processManager.AverageMemoryUsage - data['PeakMemoryUsage'] = self.processManager.AverageMemoryUsage - data['PercentProcessorTime'] = self.processManager.PercentProcessorTime - self.EnqueuePacket(data) + data_list = [] + self.AppendDataChannel(data_list, cayennemqtt.SYS_HARDWARE_MAKE, self.hardware.getManufacturer()) + self.AppendDataChannel(data_list, cayennemqtt.SYS_HARDWARE_MODEL, self.hardware.getModel()) + self.AppendDataChannel(data_list, cayennemqtt.SYS_OS_NAME, self.oSInfo.ID) + self.AppendDataChannel(data_list, cayennemqtt.SYS_OS_VERSION, self.oSInfo.VERSION_ID) + self.AppendDataChannel(data_list, cayennemqtt.SYS_AGENT_VERSION, self.config.get('Agent','Version')) + self.EnqueuePacket(data_list) + # data = {} + # data['MachineName'] = self.MachineId + # data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value + # data['IpAddress'] = self.PublicIP + # data['GatewayMACAddress'] = self.hardware.getMac() + # systemData = {} + # # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) + # # systemData['AntiVirus'] = 'None' + # # systemData['Firewall'] = 'iptables' + # # systemData['FirewallEnabled'] = 'true' + # systemData['ComputerMake'] = self.hardware.getManufacturer() + # systemData['ComputerModel'] = self.hardware.getModel() + # systemData['OsName'] = self.oSInfo.ID + # # systemData['OsBuild'] = self.oSInfo.ID_LIKE + # # systemData['OsArchitecture'] = self.hardware.Revision + # systemData['OsVersion'] = self.oSInfo.VERSION_ID + # systemData['ComputerName'] = self.machineName + # systemData['AgentVersion'] = self.config.get('Agent','Version') + # systemData['GatewayMACAddress'] = self.hardware.getMac() + # systemData['OsSettings'] = SystemConfig.getConfig() + # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() + # systemData['WifiStatus'] = self.wifiManager.GetStatus() + # data['RaspberryInfo'] = systemData + # if data != self.previousSystemInfo: + # self.previousSystemInfo = data.copy() + # data['Timestamp'] = int(time()) + # self.EnqueuePacket(data) + # logJson('SendSystemInfo: ' + dumps(data), 'SendSystemInfo') + # del systemData + # del data + # data=None + except Exception: + exception('SendSystemInfo unexpected error') + + # def SendSystemUtilization(self): + # """Enqueue a packet containing system utilization data to send to the server""" + # data = {} + # data['MachineName'] = self.MachineId + # data['Timestamp'] = int(time()) + # data['PacketType'] = PacketTypes.PT_UTILIZATION.value + # self.processManager.RefreshProcessManager() + # data['VisibleMemory'] = self.processManager.VisibleMemory + # data['AvailableMemory'] = self.processManager.AvailableMemory + # data['AverageProcessorUsage'] = self.processManager.AverageProcessorUsage + # data['PeakProcessorUsage'] = self.processManager.PeakProcessorUsage + # data['AverageMemoryUsage'] = self.processManager.AverageMemoryUsage + # data['PeakMemoryUsage'] = self.processManager.AverageMemoryUsage + # data['PercentProcessorTime'] = self.processManager.PercentProcessorTime + # self.EnqueuePacket(data) def SendSystemState(self): """Enqueue a packet containing system information to send to the server""" try: # debug('SendSystemState') - self.SendSystemInfo() - self.SendSystemUtilization() + # self.SendSystemInfo() + # self.SendSystemUtilization() data = {} data['MachineName'] = self.MachineId data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value @@ -411,7 +426,7 @@ def SendSystemState(self): systemData['SensorsInfo'] = self.sensorsClient.currentSensorsInfo systemData['BusInfo'] = self.sensorsClient.currentBusInfo systemData['OsSettings'] = SystemConfig.getConfig() - systemData['NetworkId'] = WifiManager.Network.GetNetworkId() + # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() systemData['WifiStatus'] = self.wifiManager.GetStatus() try: history = History() @@ -583,10 +598,10 @@ def ExecuteMessage(self, messageObject): return info("ExecuteMessage: " + str(messageObject['PacketType'])) packetType = int(messageObject['PacketType']) - if packetType == PacketTypes.PT_UTILIZATION.value: - self.SendSystemUtilization() - info(PacketTypes.PT_UTILIZATION) - return + # if packetType == PacketTypes.PT_UTILIZATION.value: + # self.SendSystemUtilization() + # info(PacketTypes.PT_UTILIZATION) + # return if packetType == PacketTypes.PT_SYSTEM_INFO.value: info("ExecuteMessage - sysinfo - Calling SendSystemState") self.SendSystemState() @@ -849,7 +864,8 @@ def ProcessDeviceCommand(self, messageObject): def EnqueuePacket(self, message): """Enqueue a message packet to send to the server""" - message['PacketTime'] = GetTime() + if isinstance(message, dict): + message['PacketTime'] = GetTime() json_data = dumps(message) + '\n' message = None self.writeQueue.put(json_data) diff --git a/myDevices/wifi/WifiManager.py b/myDevices/wifi/WifiManager.py index 9d062e6..069dd3a 100644 --- a/myDevices/wifi/WifiManager.py +++ b/myDevices/wifi/WifiManager.py @@ -3,29 +3,29 @@ from myDevices.utils.logger import exception, info, warn, error, debug from myDevices.utils.subprocess import executeCommand -class Network(): - def GetNetworkId(): - ip = None - network = None - returnValue = {} - try: - import netifaces - gws=netifaces.gateways() - defaultNet = gws['default'].values() - for key, val in defaultNet: - ip = key - network = val - command = 'arp -n ' + ip + ' | grep ' + network + ' | awk \'{print $3}\'' - (output, retCode) = executeCommand(command) - if int(retCode) > 0: - return None - returnValue['Ip'] = ip - returnValue['Network'] = network - returnValue['MAC'] = output.strip() - del output - except Exception as ex: - debug('Could not initialize netifaces module: ' + str(ex)) - return returnValue +# class Network(): +# def GetNetworkId(): +# ip = None +# network = None +# returnValue = {} +# try: +# import netifaces +# gws=netifaces.gateways() +# defaultNet = gws['default'].values() +# for key, val in defaultNet: +# ip = key +# network = val +# command = 'arp -n ' + ip + ' | grep ' + network + ' | awk \'{print $3}\'' +# (output, retCode) = executeCommand(command) +# if int(retCode) > 0: +# return None +# returnValue['Ip'] = ip +# returnValue['Network'] = network +# returnValue['MAC'] = output.strip() +# del output +# except Exception as ex: +# debug('Could not initialize netifaces module: ' + str(ex)) +# return returnValue #{'stats': {'updated': 75, 'noise': 0, 'quality': 67, 'level': 213}, 'Frequency': b'2.412 GHz', 'Access Point': b'B8:55:10:AC:8F:D8', 'Mode': b'Master', 'Key': b'off', 'BitRate': b'54 Mb/s', 'ESSID': b'SRX-WR300WH'} class WifiEndpoint(object): From 875de8556cdd99dd696fedd3a14f533fe2f1d655 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 10 Oct 2017 15:32:33 -0600 Subject: [PATCH 063/129] Send network speed info using new data channel. Update agent version channel. --- myDevices/cloud/cayennemqtt.py | 3 ++- myDevices/cloud/client.py | 7 ++++++- myDevices/cloud/download_speed.py | 3 +++ setup.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 50be0c9..c2ba93c 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -13,9 +13,10 @@ SYS_HARDWARE_MODEL = 'sys:hw:model' SYS_OS_NAME = 'sys:os:name' SYS_OS_VERSION = 'sys:os:version' -SYS_AGENT_VERSION = 'sys:agent:version' SYS_ETHERNET_ADDRESS = 'sys:eth:{};address' SYS_ETHERNET_SPEED = 'sys:eth:{};speed' +AGENT_VERSION = 'agent:version' + class CayenneMQTTClient: """Cayenne MQTT Client class. diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index c268304..f80bb25 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -342,7 +342,7 @@ def SendSystemInfo(self): self.AppendDataChannel(data_list, cayennemqtt.SYS_HARDWARE_MODEL, self.hardware.getModel()) self.AppendDataChannel(data_list, cayennemqtt.SYS_OS_NAME, self.oSInfo.ID) self.AppendDataChannel(data_list, cayennemqtt.SYS_OS_VERSION, self.oSInfo.VERSION_ID) - self.AppendDataChannel(data_list, cayennemqtt.SYS_AGENT_VERSION, self.config.get('Agent','Version')) + self.AppendDataChannel(data_list, cayennemqtt.AGENT_VERSION, self.config.get('Agent','Version')) self.EnqueuePacket(data_list) # data = {} # data['MachineName'] = self.MachineId @@ -400,6 +400,11 @@ def SendSystemState(self): # debug('SendSystemState') # self.SendSystemInfo() # self.SendSystemUtilization() + data_list = [] + download_speed = self.downloadSpeed.getDownloadSpeed() + if download_speed: + self.AppendDataChannel(data_list, cayennemqtt.SYS_ETHERNET_SPEED.format(self.downloadSpeed.interface), download_speed) + self.EnqueuePacket(data_list) data = {} data['MachineName'] = self.MachineId data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index 3b14cde..fd0f828 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -1,6 +1,7 @@ """ This module provides a class for testing download speed """ +import netifaces from datetime import datetime, timedelta from os import path, remove from urllib import request, error @@ -25,6 +26,7 @@ def __init__(self, config): self.downloadSpeed = None self.testTime = None self.isRunning = False + self.interface = None self.Start() self.config = config #add a random delay to the start of download @@ -49,6 +51,7 @@ def Test(self): def TestDownload(self): """Test download speed by retrieving a file""" try: + self.interface = netifaces.gateways()['default'][netifaces.AF_INET][1] a = datetime.now() info('Excuting regular download test for network speed') url = self.config.cloudConfig.DownloadSpeedTestUrl if 'DownloadSpeedTestUrl' in self.config.cloudConfig else defaultUrl diff --git a/setup.py b/setup.py index aea8b8f..cf4a146 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ url = 'https://www.mydevices.com/', classifiers = classifiers, packages = ["myDevices", "myDevices.cloud", "myDevices.utils", "myDevices.system", "myDevices.sensors" , "myDevices.wifi", "myDevices.schedule", "myDevices.requests_futures", "myDevices.devices", "myDevices.devices.analog", "myDevices.devices.digital", "myDevices.devices.sensor", "myDevices.devices.shield", "myDevices.decorators"], - install_requires = ['enum34', 'iwlib', 'jsonpickle', 'netifaces', 'psutil >= 0.7.0', 'requests'], + install_requires = ['enum34', 'iwlib', 'jsonpickle', 'netifaces >= 0.10.5', 'psutil >= 0.7.0', 'requests'], data_files = [('/etc/myDevices/scripts', ['scripts/config.sh'])] ) From a71b4f63c7130b0ff69ec2a4d269ce023de3a9fd Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 10 Oct 2017 17:59:05 -0600 Subject: [PATCH 064/129] Get network info using new data channels. --- myDevices/cloud/cayennemqtt.py | 26 ++++++++++++- myDevices/cloud/client.py | 67 +++++++++++++++------------------ myDevices/system/systeminfo.py | 68 ++++++++++++++-------------------- myDevices/wifi/WifiManager.py | 63 ++++++++++++++++--------------- 4 files changed, 112 insertions(+), 112 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index c2ba93c..1f05189 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -13,10 +13,32 @@ SYS_HARDWARE_MODEL = 'sys:hw:model' SYS_OS_NAME = 'sys:os:name' SYS_OS_VERSION = 'sys:os:version' -SYS_ETHERNET_ADDRESS = 'sys:eth:{};address' -SYS_ETHERNET_SPEED = 'sys:eth:{};speed' +SYS_ETHERNET = 'sys:eth' +SYS_WIFI = 'sys:wifi' +SYS_STORAGE = 'sys:storage' AGENT_VERSION = 'agent:version' +# Channel Suffixes +ADDRESS = 'address' +SPEED = 'speed' +SSID = 'ssid' +USAGE = 'usage' +CAPACITY = 'capacity' + +class DataChannel: + @staticmethod + def add(data_list, prefix, channel=None, suffix=None, value=None): + """Create data channel dict and append it to a list""" + data_channel = prefix + if channel: + data_channel += ':' + channel + if suffix: + data_channel += ';' + suffix + data = {} + data['channel'] = data_channel + data['value'] = value + data_list.append(data) + class CayenneMQTTClient: """Cayenne MQTT Client class. diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index f80bb25..0c866f7 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -16,7 +16,7 @@ from myDevices.system import services, ipgetter from myDevices.sensors import sensors from myDevices.system.hardware import Hardware -from myDevices.wifi import WifiManager +# from myDevices.wifi import WifiManager from myDevices.cloud.scheduler import SchedulerEngine from myDevices.cloud.download_speed import DownloadSpeed from myDevices.cloud.updater import Updater @@ -278,7 +278,7 @@ def Initialize(self): self.sensorsClient.SetDataChanged(self.OnDataChanged, self.SendSystemState) self.processManager = services.ProcessManager() self.serviceManager = services.ServiceManager() - self.wifiManager = WifiManager.WifiManager() + # self.wifiManager = WifiManager.WifiManager() self.writerThread = WriterThread('writer', self) self.writerThread.start() self.processorThread = ProcessorThread('processor', self) @@ -326,23 +326,16 @@ def OnDataChanged(self, systemData): del data del systemData - def AppendDataChannel(self, data_list, channel, value): - """Create data dict and append it to a list""" - data = {} - data['channel'] = channel - data['value'] = value - data_list.append(data) - def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" try: # debug('SendSystemInfo') data_list = [] - self.AppendDataChannel(data_list, cayennemqtt.SYS_HARDWARE_MAKE, self.hardware.getManufacturer()) - self.AppendDataChannel(data_list, cayennemqtt.SYS_HARDWARE_MODEL, self.hardware.getModel()) - self.AppendDataChannel(data_list, cayennemqtt.SYS_OS_NAME, self.oSInfo.ID) - self.AppendDataChannel(data_list, cayennemqtt.SYS_OS_VERSION, self.oSInfo.VERSION_ID) - self.AppendDataChannel(data_list, cayennemqtt.AGENT_VERSION, self.config.get('Agent','Version')) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_HARDWARE_MAKE, value=self.hardware.getManufacturer()) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_HARDWARE_MODEL, value=self.hardware.getModel()) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent','Version')) self.EnqueuePacket(data_list) # data = {} # data['MachineName'] = self.MachineId @@ -403,7 +396,7 @@ def SendSystemState(self): data_list = [] download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: - self.AppendDataChannel(data_list, cayennemqtt.SYS_ETHERNET_SPEED.format(self.downloadSpeed.interface), download_speed) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_ETHERNET, self.downloadSpeed.interface, value=download_speed) self.EnqueuePacket(data_list) data = {} data['MachineName'] = self.MachineId @@ -432,7 +425,7 @@ def SendSystemState(self): systemData['BusInfo'] = self.sensorsClient.currentBusInfo systemData['OsSettings'] = SystemConfig.getConfig() # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() - systemData['WifiStatus'] = self.wifiManager.GetStatus() + # systemData['WifiStatus'] = self.wifiManager.GetStatus() try: history = History() history.SaveAverages(systemData) @@ -782,27 +775,27 @@ def ProcessDeviceCommand(self, messageObject): sensorId = messageObject['SensorId'] data = {} retValue = '' - if commandService == 'wifi': - if commandType == 'status': - retValue = self.wifiManager.GetStatus() - if commandType == 'scan': - retValue = self.wifiManager.GetWirelessNetworks() - if commandType == 'setup': - try: - ssid = parameters["ssid"] - password = parameters["password"] - interface = parameters["interface"] - retValue = self.wifiManager.Setup(ssid, password, interface) - except: - retValue = False - if commandService == 'services': - serviceName = parameters['ServiceName'] - if commandType == 'status': - retValue = self.serviceManager.Status(serviceName) - if commandType == 'start': - retValue = self.serviceManager.Start(serviceName) - if commandType == 'stop': - retValue = self.serviceManager.Stop(serviceName) + # if commandService == 'wifi': + # if commandType == 'status': + # retValue = self.wifiManager.GetStatus() + # if commandType == 'scan': + # retValue = self.wifiManager.GetWirelessNetworks() + # if commandType == 'setup': + # try: + # ssid = parameters["ssid"] + # password = parameters["password"] + # interface = parameters["interface"] + # retValue = self.wifiManager.Setup(ssid, password, interface) + # except: + # retValue = False + # if commandService == 'services': + # serviceName = parameters['ServiceName'] + # if commandType == 'status': + # retValue = self.serviceManager.Status(serviceName) + # if commandType == 'start': + # retValue = self.serviceManager.Start(serviceName) + # if commandType == 'stop': + # retValue = self.serviceManager.Stop(serviceName) if commandService == 'sensor': debug('SENSOR_COMMAND processing: ' + str(parameters)) method = None diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index 7467c23..7f44602 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -6,7 +6,8 @@ import netifaces from myDevices.utils.logger import exception from myDevices.system.cpu import CpuInfo - +from myDevices.cloud import cayennemqtt +from myDevices.wifi import WifiManager class SystemInfo(): """Class to get system CPU, memory, uptime, storage and network info""" @@ -131,51 +132,36 @@ def getDiskInfo(self): return info def getNetworkInfo(self): - """Get network information as a dict - - Returned dict example:: - - { - "eth0": { - "ip": { - "address": "192.168.0.25", - }, - "ipv6": [{ - "address": "2001:db8:3c4d::1a2f:1a2b", - }], - "mac": "aa:bb:cc:dd:ee:ff", - }, - "wlan0": { - "ipv6": [{ - "address": "2001:db8:3c4d::1a2f:1a2b", - }], - "mac": "aa:bb:cc:dd:ee:ff" - } - } + """Get network information as a list formatted for Cayenne MQTT + + Returned list example:: + + [{ + 'channel': 'sys:eth:eth0;address' + 'value': '192.168.0.2', + }, { + 'channel': 'sys:wifi:wlan0;address' + 'value': '192.168.0.3', + }, { + 'channel': 'sys:wifi:wlan0;ssid' + 'value': 'myWifi', + }] """ - network_info = {} + network_data = [] try: - for interface in netifaces.interfaces(): + wifi_manager = WifiManager.WifiManager() + wifi_status = wifi_manager.GetStatus() + for interface in wifi_status.keys(): + cayennemqtt.DataChannel.add(network_data, cayennemqtt.SYS_WIFI, interface, cayennemqtt.SSID, wifi_status[interface]['ssid']) + interfaces = (interface for interface in netifaces.interfaces() if interface != 'lo') + for interface in interfaces: addresses = netifaces.ifaddresses(interface) - interface_info = {} try: addr = addresses[netifaces.AF_INET][0]['addr'] - interface_info['ip'] = {} - interface_info['ip']['address'] = addr - except: - pass - try: - interface_info['ipv6'] = [{'address': addr['addr'].split('%')[0]} for addr in addresses[netifaces.AF_INET6]] - except: - pass - try: - interface_info['mac'] = addresses[netifaces.AF_LINK][0]['addr'] + prefix = cayennemqtt.SYS_WIFI if interface in wifi_status else cayennemqtt.SYS_ETHERNET + data_channel.add(network_data, prefix, interface, cayennemqtt.ADDRESS, addr) except: - pass - if interface_info: - network_info[interface] = interface_info + exception('exception') except: exception('Error getting network info') - info = {} - info['list'] = network_info - return info + return network_data diff --git a/myDevices/wifi/WifiManager.py b/myDevices/wifi/WifiManager.py index 069dd3a..637c798 100644 --- a/myDevices/wifi/WifiManager.py +++ b/myDevices/wifi/WifiManager.py @@ -81,7 +81,6 @@ def Search(self, interface): exception('Wifi search address') return None - def Setup(self, ssid, password, interface): if interface in self.wirelessModules: status = self.wirelessModules[interface].connect(ssid, password) @@ -97,22 +96,23 @@ def GetIpAddress(self, interface): exception('GetIpAddress failed') return ip_addr + def GetCurretSSID(self, interface): if interface in self.wirelessModules: return self.wirelessModules[interface].current() return None + def GetDriver(self, interface): if interface in self.wirelessModules: return self.wirelessModules[interface].driver() return None + def GetPowerStatus(self, interface): if interface in self.wirelessModules: return self.wirelessModules[interface].power() return None - - - def GetStatus(self): - + + def GetStatus(self): try: jsonDictionary = {} interfaces = self.Interfaces() @@ -137,8 +137,7 @@ def GetStatus(self): driver = self.GetDriver(i) bitRate = "" stats = "" - frequency = "" - + frequency = "" if ssid is None: ssid = "" else: @@ -147,7 +146,7 @@ def GetStatus(self): if endpoint["ESSID"].decode('ascii') == ssid: frequency = endpoint["Frequency"].decode('ascii') bitRate = endpoint["BitRate"].decode('ascii') - stats = ToJson(endpoint["stats"]) + stats = endpoint["stats"] jsonDictionary[str(i)]["ssid"] = ssid jsonDictionary[str(i)]["PowerStatus"] = str(powerStatus) jsonDictionary[str(i)]["Frequency"] = str(frequency) @@ -155,7 +154,7 @@ def GetStatus(self): jsonDictionary[str(i)]["stats"] = str(stats) except Exception as ex: debug('GetStatus: failed address: ' + str(ex)) - return ToJson(jsonDictionary) + return jsonDictionary def GetWirelessNetworks(self): jsonDictionary = {} @@ -171,32 +170,32 @@ def GetWirelessNetworks(self): jsonDictionary[str(i)] = wifiEndpoints except Exception as ex: debug('GetWirelessNetworks failed: ' + str(ex)) - return ToJson(jsonDictionary) + return jsonDictionary -def ToJson(object): - returnValue = "{}" - try: - import jsonpickle - returnValue = jsonpickle.encode(object) - except: - exception('Json encoding failed') - return returnValue +# def ToJson(object): +# returnValue = "{}" +# try: +# import jsonpickle +# returnValue = jsonpickle.encode(object) +# except: +# exception('Json encoding failed') +# return returnValue -def testWifiManager(): - wifiManager = WifiManager() - info(ToJson(wifiManager.Interfaces())) - info(str(wifiManager.GetWirelessNetworks())) - info(str(wifiManager.GetCurretSSID('wlan0'))) - info(str(wifiManager.GetDriver('wlan0'))) - info(str(wifiManager.GetPowerStatus('wlan0'))) - info(str(wifiManager.GetStatus())) +# def testWifiManager(): +# wifiManager = WifiManager() +# info(wifiManager.Interfaces()) +# info(str(wifiManager.GetWirelessNetworks())) +# info(str(wifiManager.GetCurretSSID('wlan0'))) +# info(str(wifiManager.GetDriver('wlan0'))) +# info(str(wifiManager.GetPowerStatus('wlan0'))) +# info(str(wifiManager.GetStatus())) - SetBadNetwork(wifiManager) +# SetBadNetwork(wifiManager) -def SetBadNetwork(wifiManager): - info('============SETUP TESTS============') - info('Bad password test: ' + str(wifiManager.Setup('Lizuca&Patrocle', 'badpasswd'))) - info('Bad network test: ' + str(wifiManager.Setup('None', 'badpasswd'))) - info('Success setup test: ' + str(wifiManager.Setup('Lizuca&Patrocle', 'fatadraganufitrista'))) +# def SetBadNetwork(wifiManager): +# info('============SETUP TESTS============') +# info('Bad password test: ' + str(wifiManager.Setup('Lizuca&Patrocle', 'badpasswd'))) +# info('Bad network test: ' + str(wifiManager.Setup('None', 'badpasswd'))) +# info('Success setup test: ' + str(wifiManager.Setup('Lizuca&Patrocle', 'fatadraganufitrista'))) #testWifiManager() From cc8eb9ece3f6a64589962e7c3f96364d0f4a0674 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 11 Oct 2017 15:19:21 -0600 Subject: [PATCH 065/129] Send system info using new data channels. --- myDevices/cloud/cayennemqtt.py | 8 +- myDevices/system/cpu.py | 17 ++- myDevices/system/systeminfo.py | 180 ++++++++++++++---------------- myDevices/test/systeminfo_test.py | 53 +++------ 4 files changed, 116 insertions(+), 142 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 1f05189..d9b1b50 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -16,14 +16,18 @@ SYS_ETHERNET = 'sys:eth' SYS_WIFI = 'sys:wifi' SYS_STORAGE = 'sys:storage' +SYS_RAM = 'sys:ram' +SYS_CPU = 'sys:cpu' AGENT_VERSION = 'agent:version' # Channel Suffixes -ADDRESS = 'address' -SPEED = 'speed' +IP = 'ip' +SPEEDTEST = 'speedtest' SSID = 'ssid' USAGE = 'usage' CAPACITY = 'capacity' +LOAD = 'load' +TEMPERATURE = 'temp' class DataChannel: @staticmethod diff --git a/myDevices/system/cpu.py b/myDevices/system/cpu.py index 0b9f36a..1df45fa 100644 --- a/myDevices/system/cpu.py +++ b/myDevices/system/cpu.py @@ -7,7 +7,8 @@ class CpuInfo(object): """Class for retrieving CPU info""" - def get_cpu_info(self): + @staticmethod + def get_cpu_info(): """Return CPU temperature, load average and usage info as a dict""" info = {} info['temperature'] = self.get_cpu_temp() @@ -15,7 +16,8 @@ def get_cpu_info(self): info["usage"] = self.get_cpu_usage() return info - def get_cpu_usage(self): + @staticmethod + def get_cpu_usage(): """Return dict with overall CPU usage""" usage = {} try: @@ -25,8 +27,9 @@ def get_cpu_usage(self): except: exception('Error getting CPU usage info') return usage - - def get_cpu_load(self, interval = 1): + + @staticmethod + def get_cpu_load(interval = 1): """Return CPU load :param interval: time interval in seconds to wait when calculating CPU usage @@ -39,7 +42,8 @@ def get_cpu_load(self, interval = 1): exception('Error getting CPU load info') return cpu_load - def get_cpu_temp(self): + @staticmethod + def get_cpu_temp(): """Get CPU temperature""" info = {} thermal_dirs = glob('/sys/class/thermal/thermal_zone*') @@ -62,7 +66,8 @@ def get_cpu_temp(self): exception('Error getting CPU temperature') return temp - def get_load_avg(self): + @staticmethod + def get_load_avg(): """Get CPU average load for the last one, five, and 10 minute periods""" info = {} file = "/proc/loadavg" diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index 7f44602..b08cd61 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -14,122 +14,114 @@ class SystemInfo(): def getSystemInformation(self): """Get a dict containing CPU, memory, uptime, storage and network info""" - system_info = {} + system_info = [] try: - cpu_info = CpuInfo() - system_info['Cpu'] = cpu_info.get_cpu_info() - system_info['CpuLoad'] = cpu_info.get_cpu_load() - system_info['Memory'] = self.getMemoryInfo() - system_info['Uptime'] = self.getUptime() - system_info['Storage'] = self.getDiskInfo() - system_info['Network'] = self.getNetworkInfo() + system_info += self.getCpuInfo() + system_info += self.getMemoryInfo() + system_info += self.getDiskInfo() + system_info += self.getNetworkInfo() except: exception('Error retrieving system info') return system_info - def getMemoryInfo(self): - """Get a dict containing the memory info - - Returned dict example:: - - { - 'used': 377036800, - 'total': 903979008, - 'buffers': 129654784, - 'cached': 135168000, - 'processes': 112214016, - 'free': 526942208, - 'swap': { - 'used': 0, - 'free': 104853504, - 'total': 104853504 - } - } + def getCpuInfo(self): + """Get CPU information as a list formatted for Cayenne MQTT + + Returned list example:: + + [{ + 'channel': 'sys:cpu;load', + 'value': 12.8 + }, { + 'channel': 'sys:cpu;temp', + 'value': 50.843 + }] """ - memory = {} + cpu_info = [] try: - vmem = psutil.virtual_memory() - memory['total'] = vmem.total - memory['free'] = vmem.available - memory['used'] = memory['total'] - memory['free'] - memory['buffers'] = vmem.buffers - memory['cached'] = vmem.cached - memory['processes'] = memory['used'] - swap = psutil.swap_memory() - memory['swap'] = {} - memory['swap']['total'] = swap.total - memory['swap']['free'] = swap.free - memory['swap']['used'] = swap.used + cayennemqtt.DataChannel.add(cpu_info, cayennemqtt.SYS_CPU, suffix=cayennemqtt.LOAD, value=psutil.cpu_percent(1)) + cayennemqtt.DataChannel.add(cpu_info, cayennemqtt.SYS_CPU, suffix=cayennemqtt.TEMPERATURE, value=CpuInfo.get_cpu_temp()) except: - exception('Error getting memory info') - return memory + exception('Error getting CPU info') + return cpu_info - def getUptime(self): - """Get system uptime as a dict + def getMemoryInfo(self): + """Get disk usage information as a list formatted for Cayenne MQTT - Returned dict example:: + Returned list example:: - { - 'uptime': 90844.69, - 'idle': 391082.64 - } + [{ + 'channel': 'sys:ram;capacity', + 'value': 968208384 + }, { + 'channel': 'sys:ram;usage', + 'value': 296620032 + }] """ - info = {} - uptime = 0.0 - idle = 0.0 + memory_info = [] try: - with open('/proc/uptime', 'r') as f_stat: - lines = [line.split(' ') for content in f_stat.readlines() for line in content.split('\n') if line != ''] - uptime = float(lines[0][0]) - idle = float(lines[0][1]) + vmem = psutil.virtual_memory() + cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.USAGE, value=vmem.total - vmem.available) + cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.CAPACITY, value=vmem.total) except: - exception('Error getting uptime') - info['uptime'] = uptime - return info + exception('Error getting memory info') + return memory_info + + # def getUptime(self): + # """Get system uptime as a dict + + # Returned dict example:: + + # { + # 'uptime': 90844.69, + # 'idle': 391082.64 + # } + # """ + # info = {} + # uptime = 0.0 + # idle = 0.0 + # try: + # with open('/proc/uptime', 'r') as f_stat: + # lines = [line.split(' ') for content in f_stat.readlines() for line in content.split('\n') if line != ''] + # uptime = float(lines[0][0]) + # idle = float(lines[0][1]) + # except: + # exception('Error getting uptime') + # info['uptime'] = uptime + # return info def getDiskInfo(self): - """Get disk usage info as a dict - - Returned dict example:: - - { - 'list': [{ - 'filesystem': 'ext4', - 'size': 13646516224, - 'use': 0.346063, - 'mount': '/', - 'device': '/dev/root', - 'available': 8923963392, - 'used': 4005748736 - }, { - "device": "/dev/mmcblk0p5", - "filesystem": "vfat", - "mount": "/boot", - }] - } + """Get disk usage information as a list formatted for Cayenne MQTT + + Returned list example:: + + [{ + 'channel': 'sys:storage:/;capacity', + 'value': 13646516224 + }, { + 'channel': 'sys:storage:/;usage', + 'value': 6353821696 + }, { + 'channel': 'sys:storage:/dev;capacity', + 'value': 479383552 + }, { + 'channel': 'sys:storage:/dev;usage', + 'value': 0 + }] """ - disk_list = [] + storage_info = [] try: for partition in psutil.disk_partitions(True): - disk = {} - disk['filesystem'] = partition.fstype - disk['mount'] = partition.mountpoint - disk['device'] = partition.device try: usage = psutil.disk_usage(partition.mountpoint) if usage.total: - disk['size'] = usage.total - disk['used'] = usage.used - disk['available'] = usage.free - disk['use'] = round((usage.total - usage.free) / usage.total, 6) + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used) + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total) except: pass - disk_list.append(disk) except: exception('Error getting disk info') - info = {} - info['list'] = disk_list - return info + return storage_info def getNetworkInfo(self): """Get network information as a list formatted for Cayenne MQTT @@ -147,21 +139,21 @@ def getNetworkInfo(self): 'value': 'myWifi', }] """ - network_data = [] + network_info = [] try: wifi_manager = WifiManager.WifiManager() wifi_status = wifi_manager.GetStatus() for interface in wifi_status.keys(): - cayennemqtt.DataChannel.add(network_data, cayennemqtt.SYS_WIFI, interface, cayennemqtt.SSID, wifi_status[interface]['ssid']) + cayennemqtt.DataChannel.add(network_info, cayennemqtt.SYS_WIFI, interface, cayennemqtt.SSID, wifi_status[interface]['ssid']) interfaces = (interface for interface in netifaces.interfaces() if interface != 'lo') for interface in interfaces: addresses = netifaces.ifaddresses(interface) try: addr = addresses[netifaces.AF_INET][0]['addr'] prefix = cayennemqtt.SYS_WIFI if interface in wifi_status else cayennemqtt.SYS_ETHERNET - data_channel.add(network_data, prefix, interface, cayennemqtt.ADDRESS, addr) + cayennemqtt.DataChannel.add(network_info, prefix, interface, cayennemqtt.IP, addr) except: exception('exception') except: exception('Error getting network info') - return network_data + return network_info diff --git a/myDevices/test/systeminfo_test.py b/myDevices/test/systeminfo_test.py index 8769946..03c78e7 100644 --- a/myDevices/test/systeminfo_test.py +++ b/myDevices/test/systeminfo_test.py @@ -7,46 +7,19 @@ class SystemInfoTest(unittest.TestCase): def setUp(self): # setInfo() system_info = SystemInfo() - self.info = system_info.getSystemInformation() - - def testCpuInfo(self): - cpu_info = self.info['Cpu'] - info(cpu_info) - self.assertEqual(set(cpu_info.keys()), set(('loadavg', 'usage', 'temperature'))) - self.assertEqual(set(cpu_info['loadavg'].keys()), set(('one', 'five', 'ten'))) - self.assertGreaterEqual(set(cpu_info['usage'].keys()), set(('user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'total'))) - - def testCpuLoadInfo(self): - cpu_load_info = self.info['CpuLoad'] - info(cpu_load_info) - self.assertGreaterEqual(set(cpu_load_info.keys()), set(('cpu',))) - - def testMemoryInfo(self): - memory_info = self.info['Memory'] - info(memory_info) - self.assertEqual(set(memory_info.keys()), set(('total', 'free', 'used', 'buffers', 'cached', 'processes', 'swap'))) - self.assertEqual(set(memory_info['swap'].keys()), set(('total', 'free', 'used'))) - - def testUptimeInfo(self): - uptime_info = self.info['Uptime'] - info(uptime_info) - self.assertEqual(set(uptime_info.keys()), set(('uptime',))) - - def testStorageInfo(self): - storage_info = self.info['Storage'] - info(storage_info) - self.assertEqual(set(storage_info.keys()), set(('list',))) - for item in storage_info['list']: - self.assertLessEqual(set(('device', 'filesystem', 'mount')), set(item.keys())) - - def testNetworkInfo(self): - network_info = self.info['Network'] - info(network_info) - self.assertGreaterEqual(set(network_info.keys()), set(('list',))) - for key, value in network_info['list'].items(): - self.assertTrue(value) - self.assertLessEqual(set(value.keys()), set(('ip', 'ipv6', 'mac'))) - self.assertIsNotNone(network_info['list']['eth0']['ip']['address']) + self.info = {item['channel']:item['value'] for item in system_info.getSystemInformation()} + info(self.info) + + def testSystemInfo(self): + self.assertIn('sys:cpu;load', self.info) + self.assertIn('sys:cpu;temp', self.info) + self.assertIn('sys:ram;usage', self.info) + self.assertIn('sys:ram;capacity', self.info) + self.assertIn('sys:storage:/;usage', self.info) + self.assertIn('sys:storage:/;capacity', self.info) + self.assertIn('sys:eth:eth0;ip', self.info) + self.assertIn('sys:wifi:wlan0;ip', self.info) + self.assertIn('sys:wifi:wlan0;ssid', self.info) if __name__ == '__main__': From 02202fb19aa06858302d0f4baf39f9216624cb3d Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 11 Oct 2017 16:17:30 -0600 Subject: [PATCH 066/129] Send bus info using new data channels. --- myDevices/cloud/cayennemqtt.py | 10 ++++++-- myDevices/cloud/client.py | 46 +++++++++++++++++++--------------- myDevices/sensors/sensors.py | 16 +++++++----- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index d9b1b50..f8abba3 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -18,7 +18,10 @@ SYS_STORAGE = 'sys:storage' SYS_RAM = 'sys:ram' SYS_CPU = 'sys:cpu' +SYS_BUS = 'sys:bus' +SYS_GPIO = 'sys:gpio' AGENT_VERSION = 'agent:version' +RPI_DEVICETREE = 'hw:rpi:devicetree' # Channel Suffixes IP = 'ip' @@ -28,6 +31,9 @@ CAPACITY = 'capacity' LOAD = 'load' TEMPERATURE = 'temp' +VALUE = 'value' +FUNCTION = 'function' + class DataChannel: @staticmethod @@ -35,9 +41,9 @@ def add(data_list, prefix, channel=None, suffix=None, value=None): """Create data channel dict and append it to a list""" data_channel = prefix if channel: - data_channel += ':' + channel + data_channel += ':' + str(channel) if suffix: - data_channel += ';' + suffix + data_channel += ';' + str(suffix) data = {} data['channel'] = data_channel data['value'] = value diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 0c866f7..af00e2b 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -397,33 +397,39 @@ def SendSystemState(self): download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_ETHERNET, self.downloadSpeed.interface, value=download_speed) + with self.sensorsClient.sensorMutex: + data_list += self.sensorsClient.currentSystemInfo + data_list += self.sensorsClient.currentBusInfo + config = SystemConfig.getConfig() + if config: + cayennemqtt.DataChannel.add(data_list, cayennemqtt.RPI_DEVICETREE, value=config['DeviceTree']) self.EnqueuePacket(data_list) - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value - data['Timestamp'] = int(time()) - data['IpAddress'] = self.PublicIP - data['GatewayMACAddress'] = self.hardware.getMac() - systemData = {} - systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - systemData['AntiVirus'] = 'None' - systemData['Firewall'] = 'iptables' - systemData['FirewallEnabled'] = 'true' - systemData['ComputerMake'] = self.hardware.getManufacturer() - systemData['ComputerModel'] = self.hardware.getModel() - systemData['OsName'] = self.oSInfo.ID + # data = {} + # data['MachineName'] = self.MachineId + # data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value + # data['Timestamp'] = int(time()) + # data['IpAddress'] = self.PublicIP + # data['GatewayMACAddress'] = self.hardware.getMac() + # systemData = {} + # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) + # systemData['AntiVirus'] = 'None' + # systemData['Firewall'] = 'iptables' + # systemData['FirewallEnabled'] = 'true' + # systemData['ComputerMake'] = self.hardware.getManufacturer() + # systemData['ComputerModel'] = self.hardware.getModel() + # systemData['OsName'] = self.oSInfo.ID # systemData['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID # systemData['OsArchitecture'] = self.hardware.Revision - systemData['OsVersion'] = self.oSInfo.VERSION_ID - systemData['ComputerName'] = self.machineName - systemData['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') - systemData['InstallDate'] = self.installDate - systemData['GatewayMACAddress'] = self.hardware.getMac() + # systemData['OsVersion'] = self.oSInfo.VERSION_ID + # systemData['ComputerName'] = self.machineName + # systemData['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') + # systemData['InstallDate'] = self.installDate + # systemData['GatewayMACAddress'] = self.hardware.getMac() with self.sensorsClient.sensorMutex: systemData['SystemInfo'] = self.sensorsClient.currentSystemInfo systemData['SensorsInfo'] = self.sensorsClient.currentSensorsInfo systemData['BusInfo'] = self.sensorsClient.currentBusInfo - systemData['OsSettings'] = SystemConfig.getConfig() + # systemData['OsSettings'] = SystemConfig.getConfig() # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() # systemData['WifiStatus'] = self.wifiManager.GetStatus() try: diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index d6dbb8b..ba7e921 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -21,6 +21,7 @@ from myDevices.devices import instance from myDevices.utils.types import M_JSON from myDevices.system.systeminfo import SystemInfo +from myDevices.cloud import cayennemqtt REFRESH_FREQUENCY = 5 #seconds # SENSOR_INFO_SLEEP = 0.05 @@ -266,12 +267,15 @@ def CallDeviceFunction(self, func, *args): def BusInfo(self): """Return a dict with current bus info""" - json = {} - for (bus, value) in BUSLIST.items(): - json[bus] = int(value["enabled"]) - json['GPIO'] = self.gpio.wildcard() - json['GpioMap'] = self.gpio.MAPPING - self.currentBusInfo = json + bus_info = [] + bus_items = {bus.lower():int(value["enabled"]) for (bus, value) in BUSLIST.items() if bus != 'ONEWIRE'} + for (bus, value) in bus_items.items(): + cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_BUS, suffix=bus, value=value) + gpio_state = self.gpio.wildcard() + for key, value in gpio_state.items(): + cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_GPIO, key, cayennemqtt.VALUE, value['value']) + cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_GPIO, key, cayennemqtt.FUNCTION, value['function']) + self.currentBusInfo = bus_info return self.currentBusInfo def SensorsInfo(self): From 7fb84f60bf51da36d662fc2ce91cf58bf0f178c8 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 12 Oct 2017 13:20:16 -0600 Subject: [PATCH 067/129] Get sensor info using new data channels. Update bus and network data channels. --- myDevices/cloud/cayennemqtt.py | 18 ++++-- myDevices/cloud/client.py | 4 +- myDevices/sensors/sensors.py | 103 ++++++++++++------------------ myDevices/system/systeminfo.py | 26 +++----- myDevices/test/systeminfo_test.py | 5 +- 5 files changed, 67 insertions(+), 89 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index f8abba3..d480b5d 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -13,15 +13,17 @@ SYS_HARDWARE_MODEL = 'sys:hw:model' SYS_OS_NAME = 'sys:os:name' SYS_OS_VERSION = 'sys:os:version' -SYS_ETHERNET = 'sys:eth' -SYS_WIFI = 'sys:wifi' +SYS_NET = 'sys:net' SYS_STORAGE = 'sys:storage' SYS_RAM = 'sys:ram' SYS_CPU = 'sys:cpu' -SYS_BUS = 'sys:bus' +SYS_I2C = 'sys:i2c' +SYS_SPI = 'sys:spi' +SYS_UART = 'sys:uart' +SYS_DEVICETREE = 'sys:devicetree' SYS_GPIO = 'sys:gpio' AGENT_VERSION = 'agent:version' -RPI_DEVICETREE = 'hw:rpi:devicetree' +DEV_SENSOR = 'dev' # Channel Suffixes IP = 'ip' @@ -37,7 +39,7 @@ class DataChannel: @staticmethod - def add(data_list, prefix, channel=None, suffix=None, value=None): + def add(data_list, prefix, channel=None, suffix=None, value=None, type=None, unit=None, name=None): """Create data channel dict and append it to a list""" data_channel = prefix if channel: @@ -47,6 +49,12 @@ def add(data_list, prefix, channel=None, suffix=None, value=None): data = {} data['channel'] = data_channel data['value'] = value + if type: + data['type'] = type + if unit: + data['unit'] = unit + if name: + data['name'] = name data_list.append(data) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index af00e2b..49bccd8 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -396,13 +396,13 @@ def SendSystemState(self): data_list = [] download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_ETHERNET, self.downloadSpeed.interface, value=download_speed) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) with self.sensorsClient.sensorMutex: data_list += self.sensorsClient.currentSystemInfo data_list += self.sensorsClient.currentBusInfo config = SystemConfig.getConfig() if config: - cayennemqtt.DataChannel.add(data_list, cayennemqtt.RPI_DEVICETREE, value=config['DeviceTree']) + cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_DEVICETREE, value=config['DeviceTree']) self.EnqueuePacket(data_list) # data = {} # data['MachineName'] = self.MachineId diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index ba7e921..18e67ef 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -99,7 +99,7 @@ def Monitor(self): nextTime = datetime.now() + timedelta(seconds=REFRESH_FREQUENCY) sleep(REFRESH_FREQUENCY) except: - exception("Monitoring sensors and os resources failed: " + str()) + exception('Monitoring sensors and os resources failed') debug('Monitoring sensors and os resources Finished') def MonitorSensors(self): @@ -268,9 +268,10 @@ def CallDeviceFunction(self, func, *args): def BusInfo(self): """Return a dict with current bus info""" bus_info = [] - bus_items = {bus.lower():int(value["enabled"]) for (bus, value) in BUSLIST.items() if bus != 'ONEWIRE'} + bus_channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'UART': cayennemqtt.SYS_UART} + bus_items = {bus:int(value["enabled"]) for (bus, value) in BUSLIST.items() if bus in bus_channel_map} for (bus, value) in bus_items.items(): - cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_BUS, suffix=bus, value=value) + cayennemqtt.DataChannel.add(bus_info, bus_channel_map[bus], value=value) gpio_state = self.gpio.wildcard() for key, value in gpio_state.items(): cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_GPIO, key, cayennemqtt.VALUE, value['value']) @@ -281,68 +282,46 @@ def BusInfo(self): def SensorsInfo(self): """Return a dict with current sensor states for all enabled sensors""" devices = self.GetDevices() + sensors_info = [] debug(str(time()) + ' Got devices info ' + str(self.sensorsRefreshCount)) if devices is None: - return {} - for value in devices: - sensor = instance.deviceInstance(value['name']) - if 'enabled' not in value or value['enabled'] == 1: - # sleep(SENSOR_INFO_SLEEP) - try: - if value['type'] == 'Temperature': - value['Celsius'] = self.CallDeviceFunction(sensor.getCelsius) - # value['Fahrenheit'] = self.CallDeviceFunction(sensor.getFahrenheit) - # value['Kelvin'] = self.CallDeviceFunction(sensor.getKelvin) - if value['type'] == 'Pressure': - value['Pascal'] = self.CallDeviceFunction(sensor.getPascal) - if value['type'] == 'Luminosity': - value['Lux'] = self.CallDeviceFunction(sensor.getLux) - if value['type'] == 'Distance': - value['Centimeter'] = self.CallDeviceFunction(sensor.getCentimeter) - value['Inch'] = self.CallDeviceFunction(sensor.getInch) - if value['type'] in ('ADC', 'DAC'): - value['channelCount'] = self.CallDeviceFunction(sensor.analogCount) - # value['maxInteger'] = self.CallDeviceFunction(sensor.analogMaximum) - # value['resolution'] = self.CallDeviceFunction(sensor.analogResolution) - # value['allInteger'] = self.CallDeviceFunction(sensor.analogReadAll) - # value['allVolt'] = self.CallDeviceFunction(sensor.analogReadAllVolt) - value['allFloat'] = self.CallDeviceFunction(sensor.analogReadAllFloat) - # if value['type'] == 'DAC': - # value['vref'] = self.CallDeviceFunction(sensor.analogReference) - if value['type'] == 'PWM': - value['channelCount'] = self.CallDeviceFunction(sensor.pwmCount) - # value['maxInteger'] = self.CallDeviceFunction(sensor.pwmMaximum) - # value['resolution'] = self.CallDeviceFunction(sensor.pwmResolution) - value['all'] = self.CallDeviceFunction(sensor.pwmWildcard) - if value['type'] == 'Humidity': - value['float'] = self.CallDeviceFunction(sensor.getHumidity) - value['percent'] = self.CallDeviceFunction(sensor.getHumidityPercent) - if value['type'] in ('DigitalSensor', 'DigitalActuator'): - value['value'] = self.CallDeviceFunction(sensor.read) - if value['type'] == 'GPIOPort': - value['channelCount'] = self.CallDeviceFunction(sensor.digitalCount) - value['all'] = self.CallDeviceFunction(sensor.wildcard) - if value['type'] == 'AnalogSensor': - # value['integer'] = self.CallDeviceFunction(sensor.read) - value['float'] = self.CallDeviceFunction(sensor.readFloat) - # value['volt'] = self.CallDeviceFunction(sensor.readVolt) - if value['type'] == 'ServoMotor': - value['angle'] = self.CallDeviceFunction(sensor.readAngle) - if value['type'] == 'AnalogActuator': - value['float'] = self.CallDeviceFunction(sensor.readFloat) - except: - exception("Sensor values failed: "+ value['type'] + " " + value['name']) - try: - if 'hash' in value: - value['sensor'] = value['hash'] - del value['hash'] - except KeyError: - pass + return sensors_info + for device in devices: + print(device) + sensor = instance.deviceInstance(device['name']) + if 'enabled' not in device or device['enabled'] == 1: + sensor_types = {'Temperature': {'function': 'getCelsius', 'data_args': {'type': 'temp', 'unit': 'c'}}, + 'Humidity': {'function': 'getHumidityPercent', 'data_args': {'type': 'rel_hum', 'unit': 'p'}}, + 'Pressure': {'function': 'getPascal', 'data_args': {'type': 'bp', 'unit': 'pa'}}, + 'Luminosity': {'function': 'getLux', 'data_args': {'type': 'lum', 'unit': 'lux'}}, + 'Distance': {'function': 'getCentimeter', 'data_args': {'type': 'prox', 'unit': 'cm'}}, + 'ServoMotor': {'function': 'readAngle', 'data_args': {'type': 'analog_actuator'}}, + 'DigitalSensor': {'function': 'read', 'data_args': {'type': 'digital_sensor', 'unit': 'd'}}, + 'DigitalActuator': {'function': 'read', 'data_args': {'type': 'digital_actuator', 'unit': 'd'}}, + 'AnalogSensor': {'function': 'readFloat', 'data_args': {'type': 'analog_sensor'}}, + 'AnalogActuator': {'function': 'readFloat', 'data_args': {'type': 'analog_actuator'}}} + extension_types = {'ADC': {'function': 'analogReadAllFloat'}, + 'DAC': {'function': 'analogReadAllFloat'}, + 'PWM': {'function': 'pwmWildcard'}, + 'DAC': {'function': 'wildcard'}} + if device['type'] in sensor_types: + try: + sensor_type = sensor_types[device['type']] + func = getattr(sensor, sensor_type['function']) + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['hash'], value=self.CallDeviceFunction(func), **sensor_type['data_args']) + except: + exception('Failed to get sensor data: {} {}'.format(device['type'], device['name'])) + else: + try: + extension_type = extension_types[device['type']] + func = getattr(sensor, extension_type['function']) + values = self.CallDeviceFunction(func) + for pin, value in values.items(): + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['hash'] + ':' + str(pin), cayennemqtt.VALUE, value) + except: + exception('Failed to get extension data: {} {}'.format(device['type'], device['name'])) with self.sensorMutex: - if self.currentSensorsInfo: - del self.currentSensorsInfo - self.currentSensorsInfo = None - self.currentSensorsInfo = devices + self.currentSensorsInfo = sensors_info devices = None if self.sensorsRefreshCount == 0: info('System sensors info at start '+str(self.currentSensorsInfo)) diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index b08cd61..90b3aa9 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -129,31 +129,23 @@ def getNetworkInfo(self): Returned list example:: [{ - 'channel': 'sys:eth:eth0;address' + 'channel': 'sys:net;ip' 'value': '192.168.0.2', }, { - 'channel': 'sys:wifi:wlan0;address' - 'value': '192.168.0.3', - }, { - 'channel': 'sys:wifi:wlan0;ssid' - 'value': 'myWifi', + 'channel': 'sys:net;ssid', + 'value': 'myWifi' }] """ network_info = [] try: wifi_manager = WifiManager.WifiManager() wifi_status = wifi_manager.GetStatus() - for interface in wifi_status.keys(): - cayennemqtt.DataChannel.add(network_info, cayennemqtt.SYS_WIFI, interface, cayennemqtt.SSID, wifi_status[interface]['ssid']) - interfaces = (interface for interface in netifaces.interfaces() if interface != 'lo') - for interface in interfaces: - addresses = netifaces.ifaddresses(interface) - try: - addr = addresses[netifaces.AF_INET][0]['addr'] - prefix = cayennemqtt.SYS_WIFI if interface in wifi_status else cayennemqtt.SYS_ETHERNET - cayennemqtt.DataChannel.add(network_info, prefix, interface, cayennemqtt.IP, addr) - except: - exception('exception') + default_interface = netifaces.gateways()['default'][netifaces.AF_INET][1] + for default_interface in wifi_status.keys(): + cayennemqtt.DataChannel.add(network_info, cayennemqtt.SYS_NET, suffix=cayennemqtt.SSID, value=wifi_status[default_interface]['ssid']) + addresses = netifaces.ifaddresses(default_interface) + addr = addresses[netifaces.AF_INET][0]['addr'] + cayennemqtt.DataChannel.add(network_info, cayennemqtt.SYS_NET, suffix=cayennemqtt.IP, value=addr) except: exception('Error getting network info') return network_info diff --git a/myDevices/test/systeminfo_test.py b/myDevices/test/systeminfo_test.py index 03c78e7..52cd55f 100644 --- a/myDevices/test/systeminfo_test.py +++ b/myDevices/test/systeminfo_test.py @@ -17,9 +17,8 @@ def testSystemInfo(self): self.assertIn('sys:ram;capacity', self.info) self.assertIn('sys:storage:/;usage', self.info) self.assertIn('sys:storage:/;capacity', self.info) - self.assertIn('sys:eth:eth0;ip', self.info) - self.assertIn('sys:wifi:wlan0;ip', self.info) - self.assertIn('sys:wifi:wlan0;ssid', self.info) + self.assertIn('sys:net;ip', self.info) + # self.assertIn('sys:net;ssid', self.info) if __name__ == '__main__': From 5caf8119734ae28a0e49c4fcb82027515d4193d3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 12 Oct 2017 17:57:12 -0600 Subject: [PATCH 068/129] Update monitoring code to handle new data format. --- myDevices/cloud/client.py | 36 ++++---- myDevices/sensors/sensors.py | 157 +++++--------------------------- myDevices/system/systeminfo.py | 4 +- myDevices/test/sensors_test.py | 161 +++++++++++++++------------------ 4 files changed, 116 insertions(+), 242 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 49bccd8..da6bf7c 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -275,7 +275,7 @@ def Initialize(self): self.count = 10000 self.buff = bytearray(self.count) #start thread only after init of other fields - self.sensorsClient.SetDataChanged(self.OnDataChanged, self.SendSystemState) + self.sensorsClient.SetDataChanged(self.OnDataChanged) self.processManager = services.ProcessManager() self.serviceManager = services.ServiceManager() # self.wifiManager = WifiManager.WifiManager() @@ -397,9 +397,7 @@ def SendSystemState(self): download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) - with self.sensorsClient.sensorMutex: - data_list += self.sensorsClient.currentSystemInfo - data_list += self.sensorsClient.currentBusInfo + data_list += self.sensorsClient.systemData config = SystemConfig.getConfig() if config: cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_DEVICETREE, value=config['DeviceTree']) @@ -425,24 +423,24 @@ def SendSystemState(self): # systemData['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') # systemData['InstallDate'] = self.installDate # systemData['GatewayMACAddress'] = self.hardware.getMac() - with self.sensorsClient.sensorMutex: - systemData['SystemInfo'] = self.sensorsClient.currentSystemInfo - systemData['SensorsInfo'] = self.sensorsClient.currentSensorsInfo - systemData['BusInfo'] = self.sensorsClient.currentBusInfo + # with self.sensorsClient.sensorMutex: + # systemData['SystemInfo'] = self.sensorsClient.currentSystemInfo + # systemData['SensorsInfo'] = self.sensorsClient.currentSensorsInfo + # systemData['BusInfo'] = self.sensorsClient.currentBusInfo # systemData['OsSettings'] = SystemConfig.getConfig() # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() # systemData['WifiStatus'] = self.wifiManager.GetStatus() - try: - history = History() - history.SaveAverages(systemData) - except: - exception('History error') - data['RaspberryInfo'] = systemData - self.EnqueuePacket(data) - logJson('PT_SYSTEM_INFO: ' + dumps(data), 'PT_SYSTEM_INFO') - del systemData - del data - data = None + # try: + # history = History() + # history.SaveAverages(systemData) + # except: + # exception('History error') + # data['RaspberryInfo'] = systemData + # self.EnqueuePacket(data) + # logJson('PT_SYSTEM_INFO: ' + dumps(data), 'PT_SYSTEM_INFO') + # del systemData + # del data + # data = None except Exception as e: exception('ThreadSystemInfo unexpected error: ' + str(e)) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 18e67ef..76953d6 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -32,15 +32,12 @@ class SensorsClient(): def __init__(self): """Initialize the bus and sensor info and start monitoring sensor states""" self.sensorMutex = RLock() - self.systemMutex = RLock() self.continueMonitoring = False self.onDataChanged = None self.onSystemInfo = None - self.currentBusInfo = self.previousBusInfo = None - self.currentSensorsInfo = self.previousSensorsInfo = None - self.currentSystemInfo = self.previousSystemInfo = None + self.systemData = [] + self.currentSystemState = [] self.disabledSensors = {} - self.sensorsRefreshCount = 0 self.retrievingSystemInfo = False self.disabledSensorTable = "disabled_sensors" self.systemInfoRefreshList = [] @@ -81,21 +78,17 @@ def Monitor(self): while self.continueMonitoring: try: if datetime.now() > nextTime: - self.systemData = {} - refreshTime = int(time()) - if datetime.now() > nextTimeSystemInfo: - with self.systemMutex: - if not self.retrievingSystemInfo: - ThreadPool.Submit(self.MonitorSystemInformation()) - nextTimeSystemInfo = datetime.now() + timedelta(seconds=5) + self.currentSystemState = [] + self.MonitorSystemInformation() self.MonitorSensors() self.MonitorBus() - if self.onDataChanged and self.systemData: - self.onDataChanged(self.systemData) - bResult = self.RemoveRefresh(refreshTime) - if bResult and self.onSystemInfo: - self.onSystemInfo() - self.sensorsRefreshCount += 1 + if self.currentSystemState != self.systemData: + changedSystemData = self.currentSystemState + if self.systemData: + changedSystemData = [x for x in self.currentSystemState if x not in self.systemData] + if self.onDataChanged and changedSystemData: + self.onDataChanged(changedSystemData) + self.systemData = self.currentSystemState nextTime = datetime.now() + timedelta(seconds=REFRESH_FREQUENCY) sleep(REFRESH_FREQUENCY) except: @@ -106,86 +99,29 @@ def MonitorSensors(self): """Check sensor states for changes""" if not self.continueMonitoring: return - debug(str(time()) + ' Get sensors info ' + str(self.sensorsRefreshCount)) - self.SensorsInfo() - debug(str(time()) + ' Got sensors info ' + str(self.sensorsRefreshCount)) - with self.sensorMutex: - if self.SHA_Calc(self.currentSensorsInfo) != self.SHA_Calc(self.previousSensorsInfo): - mergedSensors = self.ChangedSensorsList() - if self.previousSensorsInfo: - del self.previousSensorsInfo - self.previousSensorsInfo = None - if mergedSensors: - self.systemData['SensorsInfo'] = mergedSensors - self.previousSensorsInfo = self.currentSensorsInfo - debug(str(time()) + ' Merge sensors info ' + str(self.sensorsRefreshCount)) - - def ChangedSensorsList(self): - """Return list of changed sensors""" - if self.continueMonitoring == False: - return None - if self.previousSensorsInfo == None: - return None - if self.currentSensorsInfo == None: - return None - changedSensors = [] - previousSensorsDictionary = dict((i['sensor'], i) for i in self.previousSensorsInfo) - for item in self.currentSensorsInfo: - oldItem = None - if item['sensor'] in previousSensorsDictionary: - oldItem = previousSensorsDictionary[item['sensor']] - if self.SHA_Calc(item) != self.SHA_Calc(oldItem): - changedSensors.append(item) - else: - changedSensors.append(item) - return changedSensors + self.currentSystemState += self.SensorsInfo() def MonitorBus(self): """Check bus states for changes""" if self.continueMonitoring == False: return - debug(str(time()) + ' Get bus info ' + str(self.sensorsRefreshCount)) - self.BusInfo() - debug(str(time()) + ' Got bus info ' + str(self.sensorsRefreshCount)) - if self.SHA_Calc(self.currentBusInfo) != self.SHA_Calc(self.previousBusInfo): - self.systemData['BusInfo'] = self.currentBusInfo - if self.previousBusInfo: - del self.previousBusInfo - self.previousBusInfo = None - self.previousBusInfo = self.currentBusInfo + self.currentSystemState += self.BusInfo() def MonitorSystemInformation(self): """Check system info for changes""" if self.continueMonitoring == False: return - debug(str(time()) + ' Get system info ' + str(self.sensorsRefreshCount)) - self.SystemInformation() - debug(str(time()) + ' Got system info ' + str(self.sensorsRefreshCount)) - if self.currentSystemInfo != self.previousSystemInfo: - changedSystemInfo = {} - for key in self.currentSystemInfo.keys(): - if self.previousSystemInfo and key in self.previousSystemInfo: - if self.currentSystemInfo[key] != self.previousSystemInfo[key]: - changedSystemInfo[key] = self.currentSystemInfo[key] - else: - changedSystemInfo[key] = self.currentSystemInfo[key] - self.systemData['SystemInfo'] = changedSystemInfo - self.previousSystemInfo = self.currentSystemInfo + self.currentSystemState += self.SystemInformation() def SystemInformation(self): """Return dict containing current system info, including CPU, RAM, storage and network info""" - with self.systemMutex: - self.retrievingSystemInfo = True + newSystemInfo = [] try: systemInfo = SystemInfo() newSystemInfo = systemInfo.getSystemInformation() - if newSystemInfo: - self.currentSystemInfo = newSystemInfo - except Exception as ex: - exception('SystemInformation failed: '+str(ex)) - with self.systemMutex: - self.retrievingSystemInfo = False - return self.currentSystemInfo + except Exception: + exception('SystemInformation failed') + return newSystemInfo def SHA_Calc(self, object): """Return SHA value for an object""" @@ -276,18 +212,15 @@ def BusInfo(self): for key, value in gpio_state.items(): cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_GPIO, key, cayennemqtt.VALUE, value['value']) cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_GPIO, key, cayennemqtt.FUNCTION, value['function']) - self.currentBusInfo = bus_info - return self.currentBusInfo + return bus_info def SensorsInfo(self): - """Return a dict with current sensor states for all enabled sensors""" + """Return a list with current sensor states for all enabled sensors""" devices = self.GetDevices() sensors_info = [] - debug(str(time()) + ' Got devices info ' + str(self.sensorsRefreshCount)) if devices is None: return sensors_info for device in devices: - print(device) sensor = instance.deviceInstance(device['name']) if 'enabled' not in device or device['enabled'] == 1: sensor_types = {'Temperature': {'function': 'getCelsius', 'data_args': {'type': 'temp', 'unit': 'c'}}, @@ -320,14 +253,8 @@ def SensorsInfo(self): cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['hash'] + ':' + str(pin), cayennemqtt.VALUE, value) except: exception('Failed to get extension data: {} {}'.format(device['type'], device['name'])) - with self.sensorMutex: - self.currentSensorsInfo = sensors_info - devices = None - if self.sensorsRefreshCount == 0: - info('System sensors info at start '+str(self.currentSensorsInfo)) - debug(('New sensors info retrieved: {}').format(self.sensorsRefreshCount)) - logJson('Sensors Info updated: ' + str(self.currentSensorsInfo)) - return self.currentSensorsInfo + logJson('Sensors info: {}'.format(sensors_info)) + return sensors_info def AddSensor(self, name, description, device, args): """Add a new sensor/actuator @@ -358,7 +285,6 @@ def AddSensor(self, name, description, device, args): info('Add device returned: {}'.format(retValue)) if retValue[0] == 200: bVal = True - self.AddRefresh() except: bVal = False return bVal @@ -387,25 +313,8 @@ def EditSensor(self, name, description, device, args): with self.sensorMutex: retValue = manager.updateDevice(name, sensorEdit) info('Edit device returned: {}'.format(retValue)) - try: - hashKey = self.SHA_Calc_str(name+device) - with self.sensorMutex: - if self.currentSensorsInfo: - currentSensorsDictionary = dict((i['sensor'], i) for i in self.currentSensorsInfo) - sensorData = currentSensorsDictionary[hashKey] - sensor = sensorData[hashKey] - systemData = {} - sensor['args'] = args - sensor['description'] = description - systemData['SensorsInfo'] = [] - systemData['SensorsInfo'].append(sensor) - if self.onDataChanged: - self.onDataChanged(systemData) - except: - pass if retValue[0] == 200: bVal = True - self.AddRefresh() except: exception("Edit sensor failed") bVal = False @@ -428,7 +337,6 @@ def RemoveSensor(self, name): info('Remove device returned: {}'.format(retValue)) if retValue[0] == 200: bVal = True - self.AddRefresh() except: exception("Remove sensor failed") bVal = False @@ -468,26 +376,6 @@ def EnableSensor(self, sensor, enable): self.AddRefresh() return True - def AddRefresh(self): - """Add the time to list of system info changed times""" - self.systemInfoRefreshList.append(int(time())) - - def RemoveRefresh(self, cutoff): - """Remove times from refresh list and check if system info was changed - - Args: - cutoff: Cutoff time to use when checking for system info changes - - Returns: - True if system info has changed before cutoff, False otherwise. - """ - bReturn = False - for i in self.systemInfoRefreshList: - if i < cutoff: - self.systemInfoRefreshList.remove(i) - bReturn = True - return bReturn - def GpioCommand(self, commandType, method, channel, value): """Execute onboard GPIO command @@ -544,7 +432,6 @@ def SensorCommand(self, commandType, sensorName, sensorType, driverClass, method retVal = False info('SensorCommand: {} SensorName {} SensorType {} DriverClass {} Method {} Channel {} Value {}'.format(commandType, sensorName, sensorType, driverClass, method, channel, value) ) try: - self.AddRefresh() actuators = ('GPIOPort', 'ServoMotor', 'AnalogActuator', 'LoadSensor', 'PiFaceDigital', 'DistanceSensor', 'Thermistor', 'Photoresistor', 'LightDimmer', 'LightSwitch', 'DigitalSensor', 'DigitalActuator', 'MotorSwitch', 'RelaySwitch', 'ValveSwitch', 'MotionSensor') gpioExtensions = ('GPIOPort', 'PiFaceDigital') if driverClass is None: diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index 90b3aa9..3c097d8 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -129,8 +129,8 @@ def getNetworkInfo(self): Returned list example:: [{ - 'channel': 'sys:net;ip' - 'value': '192.168.0.2', + 'channel': 'sys:net;ip', + 'value': '192.168.0.2' }, { 'channel': 'sys:net;ssid', 'value': 'myWifi' diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index b1d50a5..1cc3ca3 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -24,108 +24,97 @@ def tearDownClass(cls): del cls.client def OnDataChanged(self, sensor_data): - if 'BusInfo' in sensor_data: - self.previousBusInfo = self.currentBusInfo - self.currentBusInfo = sensor_data['BusInfo'] - if 'SensorsInfo' in sensor_data: - self.previousSensorsInfo = self.currentSensorsInfo - self.currentSensorsInfo = sensor_data['SensorsInfo'] - if 'SystemInfo' in sensor_data: - self.previousSystemInfo = self.currentSystemInfo - self.currentSystemInfo = sensor_data['SystemInfo'] - if self.previousBusInfo and self.previousSensorsInfo and self.previousSystemInfo: + self.previousSystemData = self.currentSystemData + self.currentSystemData = sensor_data + if self.previousSystemData: self.done = True def testMonitor(self): - self.previousBusInfo = None - self.currentBusInfo = None - self.previousSensorsInfo = None - self.currentSensorsInfo = None - self.previousSystemInfo = None - self.currentSystemInfo = None + self.previousSystemData = None + self.currentSystemData = None self.done = False SensorsClientTest.client.SetDataChanged(self.OnDataChanged) - self.setChannelFunction(GPIO().pins[7], 'OUT') + # self.setChannelFunction(GPIO().pins[7], 'OUT') for i in range(5): sleep(5) - self.setChannelValue(GPIO().pins[7], i % 2) + # self.setChannelValue(GPIO().pins[7], i % 2) if self.done: break - self.assertNotEqual(self.previousSystemInfo, self.currentSystemInfo) - self.assertNotEqual(self.previousBusInfo, self.currentBusInfo) - self.assertNotEqual(self.previousSensorsInfo, self.currentSensorsInfo) + info('Changed items: {}'.format([x for x in self.currentSystemData if x not in self.previousSystemData])) + self.assertNotEqual(self.previousSystemData, self.currentSystemData) def testBusInfo(self): - bus = SensorsClientTest.client.BusInfo() - # # Compare our GPIO function values with the ones from RPi.GPIO library - # import RPi.GPIO - # RPi.GPIO.setmode(RPi.GPIO.BCM) - # port_use = {0:"GPIO.OUT", 1:"GPIO.IN", 40:"GPIO.SERIAL", 41:"GPIO.SPI", 42:"GPIO.I2C", 43:"GPIO.HARD_PWM", -1:"GPIO.UNKNOWN"} - # for gpio in range(GPIO.GPIO_COUNT): - # try: - # print('{}: {} | {}'.format(gpio, bus['GPIO'][gpio]['function'], port_use[RPi.GPIO.gpio_function(gpio)])) - # except ValueError as error: - # print('{}: {}'.format(error, gpio)) - self.assertTrue(bus) + bus = {item['channel']:item['value'] for item in SensorsClientTest.client.BusInfo()} + info('Bus info: {}'.format(bus)) + self.assertIn('sys:i2c', bus) + self.assertIn('sys:spi', bus) + self.assertIn('sys:uart', bus) + for pin in GPIO().pins: + self.assertIn('sys:gpio:{};function'.format(pin), bus) + self.assertIn('sys:gpio:{};value'.format(pin), bus) - def testSetFunction(self): - self.setChannelFunction(GPIO().pins[7], 'IN') - self.setChannelFunction(GPIO().pins[7], 'OUT') + def testSensorsInfo(self): + sensors = SensorsClientTest.client.SensorsInfo() + info('Sensors info: {}'.format(sensors)) + for sensor in sensors: + self.assertEqual('dev:', sensor['channel'][:4]) + self.assertIn('value', sensor) - def testSetValue(self): - self.setChannelFunction(GPIO().pins[7], 'OUT') - self.setChannelValue(GPIO().pins[7], 1) - self.setChannelValue(GPIO().pins[7], 0) + + # def testSetFunction(self): + # self.setChannelFunction(GPIO().pins[7], 'IN') + # self.setChannelFunction(GPIO().pins[7], 'OUT') - def testSensors(self): - #Test adding a sensor - channel = GPIO().pins[8] - testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} - compareKeys = ('args', 'description', 'device') - retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) - self.assertTrue(retValue) - retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == testSensor['name']) - for key in compareKeys: - self.assertEqual(testSensor[key], retrievedSensor[key]) - #Test updating a sensor - editedSensor = testSensor - editedSensor['args']['channel'] = GPIO().pins[5] - retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) - self.assertTrue(retValue) - retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) - for key in compareKeys: - self.assertEqual(editedSensor[key], retrievedSensor[key]) - #Test removing a sensor - retValue = SensorsClientTest.client.RemoveSensor(testSensor['name']) - self.assertTrue(retValue) - deviceNames = [device['name'] for device in SensorsClientTest.client.GetDevices()] - self.assertNotIn(testSensor['name'], deviceNames) + # def testSetValue(self): + # self.setChannelFunction(GPIO().pins[7], 'OUT') + # self.setChannelValue(GPIO().pins[7], 1) + # self.setChannelValue(GPIO().pins[7], 0) - def testSensorInfo(self): - actuator_channel = GPIO().pins[9] - light_switch_channel = GPIO().pins[9] - sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': actuator_channel}, 'name': 'test_actuator'}, - 'light_switch' : {'description': 'Light Switch', 'device': 'LightSwitch', 'args': {'gpio': 'GPIO', 'invert': True, 'channel': light_switch_channel}, 'name': 'test_light_switch'}, - # 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, - # 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} - } - for sensor in sensors.values(): - SensorsClientTest.client.AddSensor(sensor['name'], sensor['description'], sensor['device'], sensor['args']) - SensorsClientTest.client.SensorsInfo() - #Test setting sensor values - self.setSensorValue(sensors['actuator'], 1) - self.setSensorValue(sensors['actuator'], 0) - self.setSensorValue(sensors['light_switch'], 1) - self.setSensorValue(sensors['light_switch'], 0) - #Test getting analog value - # retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensors['distance']['name']) - # self.assertEqual(retrievedSensorInfo['float'], 0.0) - for sensor in sensors.values(): - self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) + # def testSensors(self): + # #Test adding a sensor + # channel = GPIO().pins[8] + # testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} + # compareKeys = ('args', 'description', 'device') + # retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) + # self.assertTrue(retValue) + # retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == testSensor['name']) + # for key in compareKeys: + # self.assertEqual(testSensor[key], retrievedSensor[key]) + # #Test updating a sensor + # editedSensor = testSensor + # editedSensor['args']['channel'] = GPIO().pins[5] + # retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) + # self.assertTrue(retValue) + # retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) + # for key in compareKeys: + # self.assertEqual(editedSensor[key], retrievedSensor[key]) + # #Test removing a sensor + # retValue = SensorsClientTest.client.RemoveSensor(testSensor['name']) + # self.assertTrue(retValue) + # deviceNames = [device['name'] for device in SensorsClientTest.client.GetDevices()] + # self.assertNotIn(testSensor['name'], deviceNames) - def testSystemInfo(self): - system_info = SensorsClientTest.client.SystemInformation() - self.assertEqual(set(system_info.keys()), set(['Storage', 'Cpu', 'CpuLoad', 'Uptime', 'Network', 'Memory'])) + # def testSensorInfo(self): + # actuator_channel = GPIO().pins[9] + # light_switch_channel = GPIO().pins[9] + # sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': actuator_channel}, 'name': 'test_actuator'}, + # 'light_switch' : {'description': 'Light Switch', 'device': 'LightSwitch', 'args': {'gpio': 'GPIO', 'invert': True, 'channel': light_switch_channel}, 'name': 'test_light_switch'}, + # # 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, + # # 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} + # } + # for sensor in sensors.values(): + # SensorsClientTest.client.AddSensor(sensor['name'], sensor['description'], sensor['device'], sensor['args']) + # SensorsClientTest.client.SensorsInfo() + # #Test setting sensor values + # self.setSensorValue(sensors['actuator'], 1) + # self.setSensorValue(sensors['actuator'], 0) + # self.setSensorValue(sensors['light_switch'], 1) + # self.setSensorValue(sensors['light_switch'], 0) + # #Test getting analog value + # # retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensors['distance']['name']) + # # self.assertEqual(retrievedSensorInfo['float'], 0.0) + # for sensor in sensors.values(): + # self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) def setSensorValue(self, sensor, value): SensorsClientTest.client.SensorCommand('integer', sensor['name'], sensor['device'], None, None, None, value) From ac212b896883e2beb926d92dd2624114343ddf58 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 13 Oct 2017 17:44:49 -0600 Subject: [PATCH 069/129] Process MQTT reboot and shutdown messages. --- myDevices/cloud/cayennemqtt.py | 40 ++++++++++++++-------- myDevices/cloud/client.py | 55 +++++++++++++++++------------- myDevices/test/cayennemqtt_test.py | 36 ++++++++++--------- 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index d480b5d..dcf05c9 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -1,5 +1,5 @@ import time -from json import loads +from json import loads, decoder from ssl import PROTOCOL_TLSv1_2 import paho.mqtt.client as mqtt from myDevices.utils.logger import debug, error, exception, info, logJson, warn @@ -7,6 +7,7 @@ # Topics DATA_TOPIC = 'data.json' COMMAND_TOPIC = 'cmd' +COMMAND_RESPONSE_TOPIC = 'cmd.res' # Data Channels SYS_HARDWARE_MAKE = 'sys:hw:make' @@ -22,6 +23,7 @@ SYS_UART = 'sys:uart' SYS_DEVICETREE = 'sys:devicetree' SYS_GPIO = 'sys:gpio' +SYS_POWER = 'sys:pwr' AGENT_VERSION = 'agent:version' DEV_SENSOR = 'dev' @@ -75,7 +77,7 @@ class CayenneMQTTClient: If it exists this callback is used as the default message handler. """ client = None - rootTopic = "" + root_topic = "" connected = False on_message = None @@ -88,7 +90,7 @@ def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', por hostname is the MQTT broker hostname. port is the MQTT broker port. """ - self.rootTopic = "v2/things/%s" % clientid + self.root_topic = "v2/things/%s" % clientid self.client = mqtt.Client(client_id=clientid, clean_session=True, userdata=self) self.client.on_connect = self.connect_callback self.client.on_disconnect = self.disconnect_callback @@ -123,7 +125,7 @@ def connect_callback(self, client, userdata, flags, rc): self.connected = True # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. - client.subscribe(self.get_topic_string(COMMAND_TOPIC)) + client.subscribe(self.get_topic_string(COMMAND_TOPIC, True)) def disconnect_callback(self, client, userdata, rc): """The callback for when the client disconnects from the server. @@ -151,13 +153,18 @@ def message_callback(self, client, userdata, msg): msg is the received message. """ try: - topic = msg.topic - if msg.topic.startswith(self.rootTopic): - topic = msg.topic[len(self.rootTopic) + 1:] - message = loads(msg.payload.decode()) - debug('message_callback: {} {}'.format(topic, message)) + message = {} + try: + message['payload'] = loads(msg.payload.decode()) + except decoder.JSONDecodeError: + message['payload'] = msg.payload.decode() + channel = msg.topic.split('/')[-1].split(';') + message['channel'] = channel[0] + if len(channel) > 1: + message['suffix'] = channel[1] + info('message_callback: {}'.format(message)) if self.on_message: - self.on_message(topic, message) + self.on_message(message) except: exception("Couldn't process: "+msg.topic+" "+str(msg.payload)) @@ -167,9 +174,14 @@ def get_topic_string(self, topic, append_wildcard=False): topic: the topic substring append_wildcard: if True append the single level topics wildcard (+)""" if append_wildcard: - return '{}/{}/+'.format(self.rootTopic, topic) + return '{}/{}/+'.format(self.root_topic, topic) else: - return '{}/{}'.format(self.rootTopic, topic) + return '{}/{}'.format(self.root_topic, topic) + + def disconnect(self): + """Disconnect from Cayenne. + """ + self.client.disconnect() def loop(self, timeout=1.0): """Process Cayenne messages. @@ -206,14 +218,14 @@ def publish_packet(self, topic, packet, qos=0, retain=False): debug('Publish to {}'.format(self.get_topic_string(topic))) self.client.publish(self.get_topic_string(topic), packet, qos, retain) - def publish_response(self, msg_id, error_message): + def publish_response(self, msg_id, error_message=None): """Send a command response to Cayenne. This should be sent when a command message has been received. msg_id is the ID of the message received. error_message is the error message to send. This should be set to None if there is no error. """ - topic = self.get_topic_string(RESPONSE_TOPIC) + topic = self.get_topic_string(COMMAND_RESPONSE_TOPIC) if error_message: payload = "error,%s=%s" % (msg_id, error_message) else: diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index da6bf7c..c0d8629 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -44,8 +44,8 @@ class PacketTypes(Enum): # PT_STARTUP_APPLICATIONS = 8 PT_START_RDS = 11 PT_STOP_RDS = 12 - PT_RESTART_COMPUTER = 25 - PT_SHUTDOWN_COMPUTER = 26 + # PT_RESTART_COMPUTER = 25 + # PT_SHUTDOWN_COMPUTER = 26 # PT_KILL_PROCESS = 27 PT_REQUEST_SCHEDULES = 40 PT_UPDATE_SCHEDULES = 41 @@ -552,7 +552,7 @@ def CheckJson(self, message): return False return True - def OnMessage(self, topic, message): + def OnMessage(self, message): """Add message from the server to the queue""" info('OnMessage: {}'.format(message)) self.readQueue.put(message) @@ -598,7 +598,14 @@ def ExecuteMessage(self, messageObject): """Execute an action described in a message object""" if not messageObject: return - info("ExecuteMessage: " + str(messageObject['PacketType'])) + channel = messageObject['channel'] + info('ExecuteMessage: {}'.format(channel)) + if channel == cayennemqtt.SYS_POWER: + if messageObject['payload'] == 'reset': + executeCommand('sudo shutdown -r now') + elif messageObject['payload'] == 'halt': + executeCommand('sudo shutdown -h now') + packetType = int(messageObject['PacketType']) # if packetType == PacketTypes.PT_UTILIZATION.value: # self.SendSystemUtilization() @@ -629,26 +636,26 @@ def ExecuteMessage(self, messageObject): # self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) # info(PacketTypes.PT_PRODUCT_INFO) # return - if packetType == PacketTypes.PT_RESTART_COMPUTER.value: - info(PacketTypes.PT_RESTART_COMPUTER) - data = {} - data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value - data['MachineName'] = self.MachineId - data['Message'] = 'Computer Restarted!' - self.EnqueuePacket(data) - command = "sudo shutdown -r now" - executeCommand(command) - return - if packetType == PacketTypes.PT_SHUTDOWN_COMPUTER.value: - info(PacketTypes.PT_SHUTDOWN_COMPUTER) - data = {} - data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value - data['MachineName'] = self.MachineId - data['Message'] = 'Computer Powered Off!' - self.EnqueuePacket(data) - command = "sudo shutdown -h now" - executeCommand(command) - return + # if packetType == PacketTypes.PT_RESTART_COMPUTER.value: + # info(PacketTypes.PT_RESTART_COMPUTER) + # data = {} + # data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value + # data['MachineName'] = self.MachineId + # data['Message'] = 'Computer Restarted!' + # self.EnqueuePacket(data) + # command = "sudo shutdown -r now" + # executeCommand(command) + # return + # if packetType == PacketTypes.PT_SHUTDOWN_COMPUTER.value: + # info(PacketTypes.PT_SHUTDOWN_COMPUTER) + # data = {} + # data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value + # data['MachineName'] = self.MachineId + # data['Message'] = 'Computer Powered Off!' + # self.EnqueuePacket(data) + # command = "sudo shutdown -h now" + # executeCommand(command) + # return if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: info('PT_AGENT_CONFIGURATION: ' + str(messageObject.Data)) self.config.setCloudConfig(messageObject.Data) diff --git a/myDevices/test/cayennemqtt_test.py b/myDevices/test/cayennemqtt_test.py index 32933c6..7b7bbfd 100644 --- a/myDevices/test/cayennemqtt_test.py +++ b/myDevices/test/cayennemqtt_test.py @@ -1,4 +1,5 @@ import unittest +import warnings import myDevices.cloud.cayennemqtt as cayennemqtt import paho.mqtt.client as mqtt from time import sleep @@ -29,12 +30,13 @@ def setUp(self): def tearDown(self): # print('tearDown') self.mqttClient.loop_stop() + self.mqttClient.disconnect() self.testClient.loop_stop() + self.testClient.disconnect() - def OnMessage(self, topic, message): - self.receivedTopic = self.mqttClient.get_topic_string(topic) + def OnMessage(self, message): self.receivedMessage = message - # print('OnMessage: {} {}'.format(self.receivedTopic, self.receivedMessage)) + # print('OnMessage: {}'.format(self.receivedMessage)) def OnTestMessage(self, client, userdata, message): self.receivedTopic = message.topic @@ -45,23 +47,25 @@ def OnTestLog(self, client, userdata, level, buf): print('OnTestLog: {}'.format(buf)) def testPublish(self): - # print('testPublish') - sentTopic = self.mqttClient.get_topic_string(cayennemqtt.DATA_TOPIC) - sentMessage = '{"publish_test":"data"}' - self.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, sentMessage) - sleep(0.5) - self.assertEqual(sentTopic, self.receivedTopic) - self.assertEqual(sentMessage, self.receivedMessage) + #Ignore warning caused by paho mqtt not closing some sockets in the destructor + with warnings.catch_warnings(): + warnings.simplefilter('ignore', ResourceWarning) + sentTopic = self.mqttClient.get_topic_string(cayennemqtt.DATA_TOPIC) + sentMessage = '{"publish_test":"data"}' + self.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, sentMessage) + sleep(0.5) + self.assertEqual(sentTopic, self.receivedTopic) + self.assertEqual(sentMessage, self.receivedMessage) def testCommand(self): - # print('testCommand') - sentTopic = self.mqttClient.get_topic_string(cayennemqtt.COMMAND_TOPIC) - sentMessage = '{"command_test":"data"}' + sentTopic = self.mqttClient.get_topic_string(cayennemqtt.COMMAND_TOPIC + '/' + cayennemqtt.SYS_POWER) + sentMessage = 'reset' #'{"command_test":"data"}' self.testClient.publish(sentTopic, sentMessage) sleep(0.5) - sentMessage = loads(sentMessage) - self.assertEqual(sentTopic, self.receivedTopic) - self.assertEqual(sentMessage, self.receivedMessage) + # sentMessage = loads(sentMessage) + self.assertEqual(cayennemqtt.SYS_POWER, self.receivedMessage['channel']) + self.assertEqual(sentMessage, self.receivedMessage['payload']) + if __name__ == "__main__": unittest.main() From b7fbb686367c00f90e911691efeb20c972698872 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 16 Oct 2017 10:47:44 -0600 Subject: [PATCH 070/129] Ignore exceptions when attempting to get SSID. --- myDevices/system/systeminfo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index 3c097d8..b00a4c6 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -141,8 +141,10 @@ def getNetworkInfo(self): wifi_manager = WifiManager.WifiManager() wifi_status = wifi_manager.GetStatus() default_interface = netifaces.gateways()['default'][netifaces.AF_INET][1] - for default_interface in wifi_status.keys(): + try: cayennemqtt.DataChannel.add(network_info, cayennemqtt.SYS_NET, suffix=cayennemqtt.SSID, value=wifi_status[default_interface]['ssid']) + except: + pass addresses = netifaces.ifaddresses(default_interface) addr = addresses[netifaces.AF_INET][0]['addr'] cayennemqtt.DataChannel.add(network_info, cayennemqtt.SYS_NET, suffix=cayennemqtt.IP, value=addr) From e0348ae0ade26348793b116ad289e38573802725 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 16 Oct 2017 12:52:18 -0600 Subject: [PATCH 071/129] Get config states using config script. Process config commands using new data channels. --- myDevices/cloud/client.py | 34 ++++++++++++++++++++--------- myDevices/sensors/sensors.py | 4 ---- myDevices/system/systemconfig.py | 34 +++++++++++------------------ myDevices/test/systemconfig_test.py | 16 ++++++++++++++ scripts/config.sh | 21 ++++++++++++++++-- 5 files changed, 72 insertions(+), 37 deletions(-) create mode 100644 myDevices/test/systemconfig_test.py diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index c0d8629..501cea1 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -400,7 +400,12 @@ def SendSystemState(self): data_list += self.sensorsClient.systemData config = SystemConfig.getConfig() if config: - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_DEVICETREE, value=config['DeviceTree']) + channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} + for key, channel in channel_map.items(): + try: + cayennemqtt.DataChannel.add(data_list, channel, value=config[key]) + except: + pass self.EnqueuePacket(data_list) # data = {} # data['MachineName'] = self.MachineId @@ -605,6 +610,8 @@ def ExecuteMessage(self, messageObject): executeCommand('sudo shutdown -r now') elif messageObject['payload'] == 'halt': executeCommand('sudo shutdown -h now') + elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): + self.ProcessConfigCommand(messageObject) packetType = int(messageObject['PacketType']) # if packetType == PacketTypes.PT_UTILIZATION.value: @@ -773,6 +780,13 @@ def ExecuteMessage(self, messageObject): return info("Skipping not required packet: " + str(packetType)) + def ProcessConfigCommand(self, messageObject): + """Process system configuration command""" + value = 1 - int(messageObject['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable + command_id = {cayennemqtt.SYS_I2C: 11, cayennemqtt.SYS_SPI: 12, cayennemqtt.SYS_UART: 13, cayennemqtt.SYS_DEVICETREE: 9} + (result, output) = SystemConfig.ExecuteConfigCommand(command_id[messageObject['channel']], value) + debug('ProcessConfigCommand: {}, result: {}, output: {}'.format(messageObject, result, output)) + def ProcessDeviceCommand(self, messageObject): """Execute a command to run on the device as specified in a message object""" commandType = messageObject['Type'] @@ -853,15 +867,15 @@ def ProcessDeviceCommand(self, messageObject): debug('ProcessDeviceCommand: ' + commandService + ' ' + method + ' ' + str(channel) + ' ' + str(value)) retValue = str(self.sensorsClient.GpioCommand(commandType, method, channel, value)) debug('ProcessDeviceCommand gpio returned value: ' + retValue) - if commandService == 'config': - try: - config_id = parameters["id"] - arguments = parameters["arguments"] - (retValue, output) = SystemConfig.ExecuteConfigCommand(config_id, arguments) - data["Output"] = output - retValue = str(retValue) - except: - exception("Exception on config") + # if commandService == 'config': + # try: + # config_id = parameters["id"] + # arguments = parameters["arguments"] + # (retValue, output) = SystemConfig.ExecuteConfigCommand(config_id, arguments) + # data["Output"] = output + # retValue = str(retValue) + # except: + # exception("Exception on config") data['Response'] = retValue data['Id'] = id data['PacketType'] = PacketTypes.PT_DEVICE_COMMAND_RESPONSE.value diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 76953d6..bfbba90 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -204,10 +204,6 @@ def CallDeviceFunction(self, func, *args): def BusInfo(self): """Return a dict with current bus info""" bus_info = [] - bus_channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'UART': cayennemqtt.SYS_UART} - bus_items = {bus:int(value["enabled"]) for (bus, value) in BUSLIST.items() if bus in bus_channel_map} - for (bus, value) in bus_items.items(): - cayennemqtt.DataChannel.add(bus_info, bus_channel_map[bus], value=value) gpio_state = self.gpio.wildcard() for key, value in gpio_state.items(): cayennemqtt.DataChannel.add(bus_info, cayennemqtt.SYS_GPIO, key, cayennemqtt.VALUE, value['value']) diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index de99cf9..06998fc 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -23,7 +23,7 @@ def ExpandRootfs(): return (returnCode, output) @staticmethod - def ExecuteConfigCommand(config_id, parameters): + def ExecuteConfigCommand(config_id, parameters=''): """Execute specified command to modify configuration Args: @@ -52,9 +52,9 @@ def RestartService(): @staticmethod def getConfig(): """Return dict containing configuration settings""" - configItem = {} + config = {} if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): - return configItem + return config # try: # (returnCode, output) = SystemConfig.ExecuteConfigCommand(17, '') # if output: @@ -67,21 +67,13 @@ def getConfig(): # del output # except: # exception('Get camera config') - - try: - (returnCode, output) = SystemConfig.ExecuteConfigCommand(10, '') - if output: - configItem['DeviceTree'] = int(output.strip()) - del output - (returnCode, output) = SystemConfig.ExecuteConfigCommand(18, '') - if output: - configItem['Serial'] = int(output.strip()) - del output - (returnCode, output) = SystemConfig.ExecuteConfigCommand(20, '') - if output: - configItem['OneWire'] = int(output.strip()) - del output - except: - exception('Get config') - info('SystemConfig: {}'.format(configItem)) - return configItem \ No newline at end of file + commands = {10: 'DeviceTree', 18: 'Serial', 20: 'OneWire', 21: 'I2C', 22: 'SPI'} + for command, name in commands.items(): + try: + (returnCode, output) = SystemConfig.ExecuteConfigCommand(command) + if output: + config[name] = 1 - int(output.strip()) #Invert the value since the config script uses 0 for enable and 1 for disable + except: + exception('Get config') + info('SystemConfig: {}'.format(config)) + return config diff --git a/myDevices/test/systemconfig_test.py b/myDevices/test/systemconfig_test.py new file mode 100644 index 0000000..3084981 --- /dev/null +++ b/myDevices/test/systemconfig_test.py @@ -0,0 +1,16 @@ +import unittest +from myDevices.system.systemconfig import SystemConfig +from myDevices.utils.logger import setInfo, info + + +class SystemConfigTest(unittest.TestCase): + def testSystemConfig(self): + config = SystemConfig.getConfig() + if config: + for item in ('DeviceTree', 'Serial', 'I2C', 'SPI'): + self.assertIn(item, config) + + +if __name__ == '__main__': + setInfo() + unittest.main() \ No newline at end of file diff --git a/scripts/config.sh b/scripts/config.sh index 9003be1..13652ac 100644 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -425,11 +425,18 @@ do_i2c() { return 0 fi + exit +} +get_i2c() { + if grep -q -E "^(device_tree_param|dtparam)=([^,]*,)*i2c(_arm)?(=(on|true|yes|1))?(,.*)?$" $CONFIG; then + echo 0 + else + echo 1 + fi +} - exit -} #arg[1] enable SPI 1/0 arg[2] load by default 1/0 #again 0 enable 1 disable do_spi() { @@ -503,6 +510,14 @@ do_spi() { fi } +get_spi() { + if grep -q -E "^(device_tree_param|dtparam)=([^,]*,)*spi(=(on|true|yes|1))?(,.*)?$" $CONFIG; then + echo 0 + else + echo 1 + fi +} + do_serial() { CURRENT_STATUS="yes" # assume ttyAMA0 output enabled if ! grep -q "^T.*:.*:respawn:.*ttyAMA0" /etc/inittab; then @@ -596,6 +611,8 @@ case "$FUN" in 18) get_serial ;; 19) do_w1 ;; 20) get_w1 ;; + 21) get_i2c ;; + 22) get_spi ;; *) echo 'N/A' && exit 1;; esac || echo "There was an error running option $FUN" if [ $ASK_TO_REBOOT = 1 ]; then From 17a9b591784abef684d9e90dff7772e836ceac5e Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 16 Oct 2017 16:01:39 -0600 Subject: [PATCH 072/129] Process GPIO function and state using new data channels. --- myDevices/cloud/client.py | 33 ++++++++++++++++++----------- myDevices/sensors/sensors.py | 38 ++++++++++------------------------ myDevices/test/sensors_test.py | 35 ++++++++++++++----------------- 3 files changed, 47 insertions(+), 59 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 501cea1..36ba6e5 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -612,7 +612,9 @@ def ExecuteMessage(self, messageObject): executeCommand('sudo shutdown -h now') elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): self.ProcessConfigCommand(messageObject) - + elif channel.startswith(cayennemqtt.SYS_GPIO): + self.ProcessGpioCommand(messageObject) + packetType = int(messageObject['PacketType']) # if packetType == PacketTypes.PT_UTILIZATION.value: # self.SendSystemUtilization() @@ -780,12 +782,19 @@ def ExecuteMessage(self, messageObject): return info("Skipping not required packet: " + str(packetType)) - def ProcessConfigCommand(self, messageObject): + def ProcessConfigCommand(self, message): """Process system configuration command""" - value = 1 - int(messageObject['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable + value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable command_id = {cayennemqtt.SYS_I2C: 11, cayennemqtt.SYS_SPI: 12, cayennemqtt.SYS_UART: 13, cayennemqtt.SYS_DEVICETREE: 9} - (result, output) = SystemConfig.ExecuteConfigCommand(command_id[messageObject['channel']], value) - debug('ProcessConfigCommand: {}, result: {}, output: {}'.format(messageObject, result, output)) + result, output = SystemConfig.ExecuteConfigCommand(command_id[message['channel']], value) + debug('ProcessConfigCommand: {}, result: {}, output: {}'.format(message, result, output)) + + def ProcessGpioCommand(self, message): + """Process GPIO command""" + info('ProcessGpioCommand: {}'.format(message)) + channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', '')) + result = self.sensorsClient.GpioCommand(message['suffix'], channel, message['payload']) + debug('ProcessGpioCommand result: {}'.format(result)) def ProcessDeviceCommand(self, messageObject): """Execute a command to run on the device as specified in a message object""" @@ -860,13 +869,13 @@ def ProcessDeviceCommand(self, messageObject): if 'SensorType' in parameters: sensorType = parameters["SensorType"] retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) - if commandService == 'gpio': - method = parameters["Method"] - channel = parameters["Channel"] - value = parameters["Value"] - debug('ProcessDeviceCommand: ' + commandService + ' ' + method + ' ' + str(channel) + ' ' + str(value)) - retValue = str(self.sensorsClient.GpioCommand(commandType, method, channel, value)) - debug('ProcessDeviceCommand gpio returned value: ' + retValue) + # if commandService == 'gpio': + # method = parameters["Method"] + # channel = parameters["Channel"] + # value = parameters["Value"] + # debug('ProcessDeviceCommand: ' + commandService + ' ' + method + ' ' + str(channel) + ' ' + str(value)) + # retValue = str(self.sensorsClient.GpioCommand(commandType, method, channel, value)) + # debug('ProcessDeviceCommand gpio returned value: ' + retValue) # if commandService == 'config': # try: # config_id = parameters["id"] diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index bfbba90..34287bb 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -372,41 +372,25 @@ def EnableSensor(self, sensor, enable): self.AddRefresh() return True - def GpioCommand(self, commandType, method, channel, value): + def GpioCommand(self, command, channel, value): """Execute onboard GPIO command Args: - commandType: Type of command to execute - method: 'POST' for setting/writing values, 'GET' for retrieving values + command: Type of command to execute channel: GPIO pin - value: Value to use for reading/writing data + value: Value to use for writing data Returns: String containing command specific return value on success, or 'failure' on failure """ - info('GpioCommand ' + commandType + ' method ' + method + ' Channel: ' + str(channel) + ' Value: ' + str(value)) - if commandType == 'function': - if method == 'POST': - debug('setFunction:' + str(channel) + ' ' + str(value)) - return str(self.gpio.setFunctionString(channel, value)) - if method == 'GET': - debug('getFunction:' + str(channel) + ' ' + str(value)) - return str(self.gpio.getFunctionString(channel)) - if commandType == 'value': - if method == 'POST': - debug('digitalWrite:' + str(channel) + ' ' + str(value)) - retVal = str(self.gpio.digitalWrite(channel, value)) - return retVal - if method == 'GET': - debug('digitalRead:' + str(channel)) - return str(self.gpio.digitalRead(channel)) - if commandType == 'integer': - if method == 'POST': - debug('portWrite:' + str(value)) - return str(self.gpio.portWrite(value)) - if method == 'GET': - debug('portRead') - return str(self.gpio.portRead()) + info('GpioCommand {}, channel {}, value {}'.format(command, channel, value)) + if command == 'function': + if value.lower() in ('in', 'input'): + return str(self.gpio.setFunctionString(channel, 'in')) + elif value.lower() in ('out', 'output'): + return str(self.gpio.setFunctionString(channel, 'out')) + elif command in ('value', ''): + return self.gpio.digitalWrite(channel, value) debug.log('GpioCommand not set') return 'failure' diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index 1cc3ca3..fffa8b3 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -34,10 +34,8 @@ def testMonitor(self): self.currentSystemData = None self.done = False SensorsClientTest.client.SetDataChanged(self.OnDataChanged) - # self.setChannelFunction(GPIO().pins[7], 'OUT') - for i in range(5): - sleep(5) - # self.setChannelValue(GPIO().pins[7], i % 2) + for i in range(25): + sleep(1) if self.done: break info('Changed items: {}'.format([x for x in self.currentSystemData if x not in self.previousSystemData])) @@ -46,9 +44,6 @@ def testMonitor(self): def testBusInfo(self): bus = {item['channel']:item['value'] for item in SensorsClientTest.client.BusInfo()} info('Bus info: {}'.format(bus)) - self.assertIn('sys:i2c', bus) - self.assertIn('sys:spi', bus) - self.assertIn('sys:uart', bus) for pin in GPIO().pins: self.assertIn('sys:gpio:{};function'.format(pin), bus) self.assertIn('sys:gpio:{};value'.format(pin), bus) @@ -61,14 +56,14 @@ def testSensorsInfo(self): self.assertIn('value', sensor) - # def testSetFunction(self): - # self.setChannelFunction(GPIO().pins[7], 'IN') - # self.setChannelFunction(GPIO().pins[7], 'OUT') + def testSetFunction(self): + self.setChannelFunction(GPIO().pins[7], 'IN') + self.setChannelFunction(GPIO().pins[7], 'OUT') - # def testSetValue(self): - # self.setChannelFunction(GPIO().pins[7], 'OUT') - # self.setChannelValue(GPIO().pins[7], 1) - # self.setChannelValue(GPIO().pins[7], 0) + def testSetValue(self): + self.setChannelFunction(GPIO().pins[7], 'OUT') + self.setChannelValue(GPIO().pins[7], 1) + self.setChannelValue(GPIO().pins[7], 0) # def testSensors(self): # #Test adding a sensor @@ -122,14 +117,14 @@ def setSensorValue(self, sensor, value): self.assertEqual(value, sensorInfo['value']) def setChannelFunction(self, channel, function): - SensorsClientTest.client.gpio.setFunctionString(channel, function) - bus = SensorsClientTest.client.BusInfo() - self.assertEqual(function, bus['GPIO'][channel]['function']) + SensorsClientTest.client.GpioCommand('function', channel, function) + bus = {item['channel']:item['value'] for item in SensorsClientTest.client.BusInfo()} + self.assertEqual(function, bus['sys:gpio:{};function'.format(channel)]) def setChannelValue(self, channel, value): - SensorsClientTest.client.gpio.digitalWrite(channel, value) - bus = SensorsClientTest.client.BusInfo() - self.assertEqual(value, bus['GPIO'][channel]['value']) + SensorsClientTest.client.GpioCommand('value', channel, value) + bus = {item['channel']:item['value'] for item in SensorsClientTest.client.BusInfo()} + self.assertEqual(value, bus['sys:gpio:{};value'.format(channel)]) if __name__ == '__main__': setInfo() From dcc62a0a8adb0fc8a46503ea84d3a71784e0b058 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 16 Oct 2017 16:25:12 -0600 Subject: [PATCH 073/129] Comment out unneeded scheduler, history and data changed code. Send config info less frequently. --- myDevices/cloud/client.py | 159 +++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 36ba6e5..051a136 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -39,7 +39,7 @@ class PacketTypes(Enum): """Packet types used when sending/receiving messages""" # PT_UTILIZATION = 3 - PT_SYSTEM_INFO = 4 + # PT_SYSTEM_INFO = 4 # PT_PROCESS_LIST = 5 # PT_STARTUP_APPLICATIONS = 8 PT_START_RDS = 11 @@ -47,8 +47,8 @@ class PacketTypes(Enum): # PT_RESTART_COMPUTER = 25 # PT_SHUTDOWN_COMPUTER = 26 # PT_KILL_PROCESS = 27 - PT_REQUEST_SCHEDULES = 40 - PT_UPDATE_SCHEDULES = 41 + # PT_REQUEST_SCHEDULES = 40 + # PT_UPDATE_SCHEDULES = 41 PT_AGENT_MESSAGE = 45 # PT_PRODUCT_INFO = 50 PT_UNINSTALL_AGENT = 51 @@ -57,13 +57,13 @@ class PacketTypes(Enum): PT_UPDATE_SENSOR = 63 PT_DEVICE_COMMAND = 64 PT_DEVICE_COMMAND_RESPONSE = 65 - PT_ADD_SCHEDULE = 66 - PT_REMOVE_SCHEDULE = 67 - PT_GET_SCHEDULES = 68 + # PT_ADD_SCHEDULE = 66 + # PT_REMOVE_SCHEDULE = 67 + # PT_GET_SCHEDULES = 68 PT_NOTIFICATION = 69 PT_DATA_CHANGED = 70 - PT_HISTORY_DATA = 71 - PT_HISTORY_DATA_RESPONSE = 72 + # PT_HISTORY_DATA = 71 + # PT_HISTORY_DATA_RESPONSE = 72 PT_AGENT_CONFIGURATION = 74 @@ -286,11 +286,11 @@ def Initialize(self): TimerThread(self.SendSystemInfo, 300) TimerThread(self.SendSystemState, 30, 5) self.previousSystemInfo = None - self.sentHistoryData = {} - self.historySendFails = 0 - self.historyThread = Thread(target=self.SendHistoryData) - self.historyThread.setDaemon(True) - self.historyThread.start() + # self.sentHistoryData = {} + # self.historySendFails = 0 + # self.historyThread = Thread(target=self.SendHistoryData) + # self.historyThread.setDaemon(True) + # self.historyThread.start() except Exception as e: exception('Initialize error: ' + str(e)) @@ -315,16 +315,17 @@ def Destroy(self): # """Send messages when client is first started""" # self.SendSystemInfo() - def OnDataChanged(self, systemData): + def OnDataChanged(self, data): """Enqueue a packet containing changed system data to send to the server""" - data = {} - data['MachineName'] = self.MachineId - data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value - data['Timestamp'] = int(time()) - data['RaspberryInfo'] = systemData self.EnqueuePacket(data) - del data - del systemData + # data = {} + # data['MachineName'] = self.MachineId + # data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value + # data['Timestamp'] = int(time()) + # data['RaspberryInfo'] = systemData + # self.EnqueuePacket(data) + # del data + # del systemData def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" @@ -336,6 +337,14 @@ def SendSystemInfo(self): cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) cayennemqtt.DataChannel.add(data_list, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent','Version')) + config = SystemConfig.getConfig() + if config: + channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} + for key, channel in channel_map.items(): + try: + cayennemqtt.DataChannel.add(data_list, channel, value=config[key]) + except: + pass self.EnqueuePacket(data_list) # data = {} # data['MachineName'] = self.MachineId @@ -398,14 +407,6 @@ def SendSystemState(self): if download_speed: cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) data_list += self.sensorsClient.systemData - config = SystemConfig.getConfig() - if config: - channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} - for key, channel in channel_map.items(): - try: - cayennemqtt.DataChannel.add(data_list, channel, value=config[key]) - except: - pass self.EnqueuePacket(data_list) # data = {} # data['MachineName'] = self.MachineId @@ -620,11 +621,11 @@ def ExecuteMessage(self, messageObject): # self.SendSystemUtilization() # info(PacketTypes.PT_UTILIZATION) # return - if packetType == PacketTypes.PT_SYSTEM_INFO.value: - info("ExecuteMessage - sysinfo - Calling SendSystemState") - self.SendSystemState() - info(PacketTypes.PT_SYSTEM_INFO) - return + # if packetType == PacketTypes.PT_SYSTEM_INFO.value: + # info("ExecuteMessage - sysinfo - Calling SendSystemState") + # self.SendSystemState() + # info(PacketTypes.PT_SYSTEM_INFO) + # return if packetType == PacketTypes.PT_UNINSTALL_AGENT.value: command = "sudo /etc/myDevices/uninstall/uninstall.sh" executeCommand(command) @@ -735,51 +736,51 @@ def ExecuteMessage(self, messageObject): info(PacketTypes.PT_DEVICE_COMMAND) self.ProcessDeviceCommand(messageObject) return - if packetType == PacketTypes.PT_ADD_SCHEDULE.value: - info(PacketTypes.PT_ADD_SCHEDULE.value) - retVal = self.schedulerEngine.AddScheduledItem(messageObject, True) - if 'Update' in messageObject: - messageObject['Update'] = messageObject['Update'] - messageObject['PacketType'] = PacketTypes.PT_ADD_SCHEDULE.value - messageObject['MachineName'] = self.MachineId - messageObject['Status'] = str(retVal) - self.EnqueuePacket(messageObject) - return - if packetType == PacketTypes.PT_REMOVE_SCHEDULE.value: - info(PacketTypes.PT_REMOVE_SCHEDULE) - retVal = self.schedulerEngine.RemoveScheduledItem(messageObject) - messageObject['PacketType'] = PacketTypes.PT_REMOVE_SCHEDULE.value - messageObject['MachineName'] = self.MachineId - messageObject['Status'] = str(retVal) - self.EnqueuePacket(messageObject) - return - if packetType == PacketTypes.PT_GET_SCHEDULES.value: - info(PacketTypes.PT_GET_SCHEDULES) - schedulesJson = self.schedulerEngine.GetSchedules() - data['Schedules'] = schedulesJson - data['PacketType'] = PacketTypes.PT_GET_SCHEDULES.value - data['MachineName'] = self.MachineId - self.EnqueuePacket(data) - return - if packetType == PacketTypes.PT_UPDATE_SCHEDULES.value: - info(PacketTypes.PT_UPDATE_SCHEDULES) - retVal = self.schedulerEngine.UpdateSchedules(messageObject) - return - if packetType == PacketTypes.PT_HISTORY_DATA_RESPONSE.value: - info(PacketTypes.PT_HISTORY_DATA_RESPONSE) - try: - id = messageObject['Id'] - history = History() - if messageObject['Status']: - history.Sent(True, self.sentHistoryData[id]['HistoryData']) - self.historySendFails = 0 - else: - history.Sent(False, self.sentHistoryData[id]['HistoryData']) - self.historySendFails += 1 - del self.sentHistoryData[id] - except: - exception('Processing history response packet failed') - return + # if packetType == PacketTypes.PT_ADD_SCHEDULE.value: + # info(PacketTypes.PT_ADD_SCHEDULE.value) + # retVal = self.schedulerEngine.AddScheduledItem(messageObject, True) + # if 'Update' in messageObject: + # messageObject['Update'] = messageObject['Update'] + # messageObject['PacketType'] = PacketTypes.PT_ADD_SCHEDULE.value + # messageObject['MachineName'] = self.MachineId + # messageObject['Status'] = str(retVal) + # self.EnqueuePacket(messageObject) + # return + # if packetType == PacketTypes.PT_REMOVE_SCHEDULE.value: + # info(PacketTypes.PT_REMOVE_SCHEDULE) + # retVal = self.schedulerEngine.RemoveScheduledItem(messageObject) + # messageObject['PacketType'] = PacketTypes.PT_REMOVE_SCHEDULE.value + # messageObject['MachineName'] = self.MachineId + # messageObject['Status'] = str(retVal) + # self.EnqueuePacket(messageObject) + # return + # if packetType == PacketTypes.PT_GET_SCHEDULES.value: + # info(PacketTypes.PT_GET_SCHEDULES) + # schedulesJson = self.schedulerEngine.GetSchedules() + # data['Schedules'] = schedulesJson + # data['PacketType'] = PacketTypes.PT_GET_SCHEDULES.value + # data['MachineName'] = self.MachineId + # self.EnqueuePacket(data) + # return + # if packetType == PacketTypes.PT_UPDATE_SCHEDULES.value: + # info(PacketTypes.PT_UPDATE_SCHEDULES) + # retVal = self.schedulerEngine.UpdateSchedules(messageObject) + # return + # if packetType == PacketTypes.PT_HISTORY_DATA_RESPONSE.value: + # info(PacketTypes.PT_HISTORY_DATA_RESPONSE) + # try: + # id = messageObject['Id'] + # history = History() + # if messageObject['Status']: + # history.Sent(True, self.sentHistoryData[id]['HistoryData']) + # self.historySendFails = 0 + # else: + # history.Sent(False, self.sentHistoryData[id]['HistoryData']) + # self.historySendFails += 1 + # del self.sentHistoryData[id] + # except: + # exception('Processing history response packet failed') + # return info("Skipping not required packet: " + str(packetType)) def ProcessConfigCommand(self, message): From 6c49ea6c25eabec4eeeb867632f03c226d06fd89 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 17 Oct 2017 11:31:29 -0600 Subject: [PATCH 074/129] Process sensor state commands using new data channels. --- myDevices/cloud/client.py | 33 +++++++++----- myDevices/sensors/sensors.py | 85 +++++++++++++----------------------- 2 files changed, 54 insertions(+), 64 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 051a136..992f825 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -615,6 +615,8 @@ def ExecuteMessage(self, messageObject): self.ProcessConfigCommand(messageObject) elif channel.startswith(cayennemqtt.SYS_GPIO): self.ProcessGpioCommand(messageObject) + elif channel.startswith(cayennemqtt.DEV_SENSOR): + self.ProcessSensorCommand(messageObject) packetType = int(messageObject['PacketType']) # if packetType == PacketTypes.PT_UTILIZATION.value: @@ -797,6 +799,17 @@ def ProcessGpioCommand(self, message): result = self.sensorsClient.GpioCommand(message['suffix'], channel, message['payload']) debug('ProcessGpioCommand result: {}'.format(result)) + def ProcessSensorCommand(self, message): + """Process sensor command""" + info('ProcessSensorCommand: {}'.format(message)) + sensor_info = message['channel'].replace(cayennemqtt.DEV_SENSOR + ':', '').split(':') + sensor = sensor_info[0] + channel = None + if len(sensor_info) > 1: + channel = sensor_info[1] + result = self.sensorsClient.SensorCommand(message['suffix'], sensor, channel, message['payload']) + debug('ProcessSensorCommand result: {}'.format(result)) + def ProcessDeviceCommand(self, messageObject): """Execute a command to run on the device as specified in a message object""" commandType = messageObject['Type'] @@ -860,16 +873,16 @@ def ProcessDeviceCommand(self, messageObject): if "Args" in parameters: args = parameters["Args"] retValue = self.sensorsClient.EditSensor(sensorName, description, driverClass, args) - else: - if 'Channel' in parameters: - channel = parameters["Channel"] - if 'Method' in parameters: - method = parameters["Method"] - if 'Value' in parameters: - value = parameters["Value"] - if 'SensorType' in parameters: - sensorType = parameters["SensorType"] - retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) + # else: + # if 'Channel' in parameters: + # channel = parameters["Channel"] + # if 'Method' in parameters: + # method = parameters["Method"] + # if 'Value' in parameters: + # value = parameters["Value"] + # if 'SensorType' in parameters: + # sensorType = parameters["SensorType"] + # retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) # if commandService == 'gpio': # method = parameters["Method"] # channel = parameters["Channel"] diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 34287bb..4e042ad 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -394,70 +394,47 @@ def GpioCommand(self, command, channel, value): debug.log('GpioCommand not set') return 'failure' - def SensorCommand(self, commandType, sensorName, sensorType, driverClass, method, channel, value): + def SensorCommand(self, command, sensorId, channel, value): """Execute sensor/actuator command Args: - commandType: Type of command to execute - sensorName: Name of the sensor - sensorType: Type of the sensor - driverClass: Class of device - method: Not currently used - channel: Pin/channel on device - value: Value to use for sending data + command: Type of command to execute + sensorId: Sensor id + channel: Pin/channel on device, None if there is no channel + value: Value to use for setting the sensor state Returns: Command specific return value on success, False on failure """ - retVal = False - info('SensorCommand: {} SensorName {} SensorType {} DriverClass {} Method {} Channel {} Value {}'.format(commandType, sensorName, sensorType, driverClass, method, channel, value) ) + result = False + info('SensorCommand: {}, sensor {}, channel {}, value {}'.format(command, sensorId, channel, value)) try: - actuators = ('GPIOPort', 'ServoMotor', 'AnalogActuator', 'LoadSensor', 'PiFaceDigital', 'DistanceSensor', 'Thermistor', 'Photoresistor', 'LightDimmer', 'LightSwitch', 'DigitalSensor', 'DigitalActuator', 'MotorSwitch', 'RelaySwitch', 'ValveSwitch', 'MotionSensor') - gpioExtensions = ('GPIOPort', 'PiFaceDigital') - if driverClass is None: - hashKey = self.SHA_Calc_str(sensorName+sensorType) - else: - hashKey = self.SHA_Calc_str(sensorName+driverClass) + commands = {'integer': {'function': 'write', 'value_type': int}, + 'value': {'function': 'write', 'value_type': int}, + 'function': {'function': 'setFunctionString', 'value_type': str}, + 'angle': {'function': 'writeAngle', 'value_type': float}, + 'float': {'function': 'writeFloat', 'value_type': float}, + 'volt': {'function': 'writeVolt', 'value_type': float}} with self.sensorMutex: - if hashKey in self.disabledSensors: - return retVal - sensor = instance.deviceInstance(sensorName) + if sensorId in self.disabledSensors: + info('Sensor disabled') + return result + sensor = instance.deviceInstance(sensorId) if not sensor: info('Sensor not found') - return retVal - if (sensorType in actuators) or (driverClass in actuators): - if sensorType in gpioExtensions or driverClass in gpioExtensions: - if commandType == 'integer' or commandType == 'value': - retVal = str(self.CallDeviceFunction(sensor.write, int(channel), int(value))) - return retVal + return result + if command in commands: + info('Sensor found: {}'.format(instance.DEVICES[sensorId])) + func = getattr(sensor, commands[command]['function']) + value = commands[command]['value_type'](value) + if channel: + result = self.CallDeviceFunction(func, int(channel), value) else: - if commandType == 'integer': - retVal = str(self.CallDeviceFunction(sensor.write, int(value))) - return retVal - if commandType == 'function': - retVal = str(self.CallDeviceFunction(sensor.setFunctionString, channel, value)) - return retVal - if commandType == 'angle': - retVal = self.CallDeviceFunction(sensor.writeAngle, value) - return retVal - if commandType == 'float': - retVal = self.CallDeviceFunction(sensor.writeFloat, float(value)) - return retVal - if commandType == 'integer': - retVal = float(self.CallDeviceFunction(sensor.write, int(channel), int(value))) - return retVal - if commandType == 'float': - retVal = float(self.CallDeviceFunction(sensor.writeFloat, int(channel), float(value))) - return retVal - if commandType == 'volt': - retVal = float(self.CallDeviceFunction(sensor.writeVolt, int(channel), float(value))) - return retVal - if commandType == 'angle': - retVal = float(self.CallDeviceFunction(sensor.writeAngle, int(channel), float(value))) - return retVal - warn('Command not implemented: ' + commandType) - return retVal - except Exception as ex: - exception('SensorCommand failed with: ' +str(ex)) - return retVal + result = self.CallDeviceFunction(func, value) + return result + warn('Command not implemented: {}'.format(command)) + return result + except Exception: + exception('SensorCommand failed') + return result From 40bf1cfe54c133e729c57ae79adb31377b5fd785 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 17 Oct 2017 16:06:59 -0600 Subject: [PATCH 075/129] Add/edit/remove sensors using new data channels. Comment out unneeded code. --- myDevices/cloud/cayennemqtt.py | 3 +- myDevices/cloud/client.py | 432 +++++++++++++++++---------------- myDevices/sensors/sensors.py | 7 +- myDevices/test/sensors_test.py | 95 ++++---- 4 files changed, 278 insertions(+), 259 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index dcf05c9..9356aa8 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -25,6 +25,7 @@ SYS_GPIO = 'sys:gpio' SYS_POWER = 'sys:pwr' AGENT_VERSION = 'agent:version' +AGENT_DEVICES = 'agent:devices' DEV_SENSOR = 'dev' # Channel Suffixes @@ -162,7 +163,7 @@ def message_callback(self, client, userdata, msg): message['channel'] = channel[0] if len(channel) > 1: message['suffix'] = channel[1] - info('message_callback: {}'.format(message)) + debug('message_callback: {}'.format(message)) if self.on_message: self.on_message(message) except: diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 992f825..1a09e5a 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -49,19 +49,19 @@ class PacketTypes(Enum): # PT_KILL_PROCESS = 27 # PT_REQUEST_SCHEDULES = 40 # PT_UPDATE_SCHEDULES = 41 - PT_AGENT_MESSAGE = 45 + # PT_AGENT_MESSAGE = 45 # PT_PRODUCT_INFO = 50 PT_UNINSTALL_AGENT = 51 - PT_ADD_SENSOR = 61 - PT_REMOVE_SENSOR = 62 - PT_UPDATE_SENSOR = 63 - PT_DEVICE_COMMAND = 64 - PT_DEVICE_COMMAND_RESPONSE = 65 + # PT_ADD_SENSOR = 61 + # PT_REMOVE_SENSOR = 62 + # PT_UPDATE_SENSORPT_UPDATE_SENSOR = 63 + # PT_DEVICE_COMMAND = 64 + # PT_DEVICE_COMMAND_RESPONSE = 65 # PT_ADD_SCHEDULE = 66 # PT_REMOVE_SCHEDULE = 67 # PT_GET_SCHEDULES = 68 PT_NOTIFICATION = 69 - PT_DATA_CHANGED = 70 + # PT_DATA_CHANGED = 70 # PT_HISTORY_DATA = 71 # PT_HISTORY_DATA_RESPONSE = 72 PT_AGENT_CONFIGURATION = 74 @@ -577,18 +577,19 @@ def RunAction(self, action): def SendNotification(self, notify, subject, body): """Enqueue a notification message packet to send to the server""" info('SendNotification: ' + str(notify) + ' ' + str(subject) + ' ' + str(body)) - try: - data = {} - data['PacketType'] = PacketTypes.PT_NOTIFICATION.value - data['MachineName'] = self.MachineId - data['Subject'] = subject - data['Body'] = body - data['Notify'] = notify - self.EnqueuePacket(data) - except: - debug('') - return False - return True + return False + # try: + # data = {} + # data['PacketType'] = PacketTypes.PT_NOTIFICATION.value + # data['MachineName'] = self.MachineId + # data['Subject'] = subject + # data['Body'] = body + # data['Notify'] = notify + # self.EnqueuePacket(data) + # except: + # debug('') + # return False + # return True def ProcessMessage(self): """Process a message from the server""" @@ -600,25 +601,26 @@ def ProcessMessage(self): return False self.ExecuteMessage(messageObject) - def ExecuteMessage(self, messageObject): + def ExecuteMessage(self, message): """Execute an action described in a message object""" - if not messageObject: + if not message: return - channel = messageObject['channel'] - info('ExecuteMessage: {}'.format(channel)) + channel = message['channel'] + info('ExecuteMessage: {}'.format(message)) if channel == cayennemqtt.SYS_POWER: - if messageObject['payload'] == 'reset': - executeCommand('sudo shutdown -r now') - elif messageObject['payload'] == 'halt': - executeCommand('sudo shutdown -h now') - elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): - self.ProcessConfigCommand(messageObject) - elif channel.startswith(cayennemqtt.SYS_GPIO): - self.ProcessGpioCommand(messageObject) + self.ProcessPowerCommand(message) elif channel.startswith(cayennemqtt.DEV_SENSOR): - self.ProcessSensorCommand(messageObject) + self.ProcessSensorCommand(message) + elif channel.startswith(cayennemqtt.SYS_GPIO): + self.ProcessGpioCommand(message) + elif channel == cayennemqtt.AGENT_DEVICES: + self.ProcessDeviceCommand(message) + elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): + self.ProcessConfigCommand(message) + else: + info('Unknown message') - packetType = int(messageObject['PacketType']) + packetType = int(message['PacketType']) # if packetType == PacketTypes.PT_UTILIZATION.value: # self.SendSystemUtilization() # info(PacketTypes.PT_UTILIZATION) @@ -669,75 +671,75 @@ def ExecuteMessage(self, messageObject): # executeCommand(command) # return if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: - info('PT_AGENT_CONFIGURATION: ' + str(messageObject.Data)) - self.config.setCloudConfig(messageObject.Data) - return - if packetType == PacketTypes.PT_ADD_SENSOR.value: - try: - info(PacketTypes.PT_ADD_SENSOR) - parameters = None - deviceName = None - deviceClass = None - description = None - #for backward compatibility check the DisplayName and overwrite it over the other variables - displayName = None - if 'DisplayName' in messageObject: - displayName = messageObject['DisplayName'] - - if 'Parameters' in messageObject: - parameters = messageObject['Parameters'] - - if 'DeviceName' in messageObject: - deviceName = messageObject['DeviceName'] - else: - deviceName = displayName - - if 'Description' in messageObject: - description = messageObject['Description'] - else: - description = deviceName - - if 'Class' in messageObject: - deviceClass = messageObject['Class'] - - retValue = True - retValue = self.sensorsClient.AddSensor(deviceName, description, deviceClass, parameters) - except Exception as ex: - exception("PT_ADD_SENSOR Unexpected error"+ str(ex)) - retValue = False - data = {} - if 'Id' in messageObject: - data['Id'] = messageObject['Id'] - #0 - None, 1 - Pending, 2-Success, 3 - Not responding, 4 - Failure - if retValue: - data['State'] = 2 - else: - data['State'] = 4 - data['PacketType'] = PacketTypes.PT_UPDATE_SENSOR.value - data['MachineName'] = self.MachineId - self.EnqueuePacket(data) - return - if packetType == PacketTypes.PT_REMOVE_SENSOR.value: - try: - info(PacketTypes.PT_REMOVE_SENSOR) - retValue = False - if 'Name' in messageObject: - Name = messageObject['Name'] - retValue = self.sensorsClient.RemoveSensor(Name) - data = {} - data['Name'] = Name - data['PacketType'] = PacketTypes.PT_REMOVE_SENSOR.value - data['MachineName'] = self.MachineId - data['Response'] = retValue - self.EnqueuePacket(data) - except Exception as ex: - exception("PT_REMOVE_SENSOR Unexpected error"+ str(ex)) - retValue = False - return - if packetType == PacketTypes.PT_DEVICE_COMMAND.value: - info(PacketTypes.PT_DEVICE_COMMAND) - self.ProcessDeviceCommand(messageObject) + info('PT_AGENT_CONFIGURATION: ' + str(message.Data)) + self.config.setCloudConfig(message.Data) return + # if packetType == PacketTypes.PT_ADD_SENSOR.value: + # try: + # info(PacketTypes.PT_ADD_SENSOR) + # parameters = None + # deviceName = None + # deviceClass = None + # description = None + # #for backward compatibility check the DisplayName and overwrite it over the other variables + # displayName = None + # if 'DisplayName' in messageObject: + # displayName = messageObject['DisplayName'] + + # if 'Parameters' in messageObject: + # parameters = messageObject['Parameters'] + + # if 'DeviceName' in messageObject: + # deviceName = messageObject['DeviceName'] + # else: + # deviceName = displayName + + # if 'Description' in messageObject: + # description = messageObject['Description'] + # else: + # description = deviceName + + # if 'Class' in messageObject: + # deviceClass = messageObject['Class'] + + # retValue = True + # retValue = self.sensorsClient.AddSensor(deviceName, description, deviceClass, parameters) + # except Exception as ex: + # exception("PT_ADD_SENSOR Unexpected error"+ str(ex)) + # retValue = False + # data = {} + # if 'Id' in messageObject: + # data['Id'] = messageObject['Id'] + # #0 - None, 1 - Pending, 2-Success, 3 - Not responding, 4 - Failure + # if retValue: + # data['State'] = 2 + # else: + # data['State'] = 4 + # data['PacketType'] = PacketTypes.PT_UPDATE_SENSOR.value + # data['MachineName'] = self.MachineId + # self.EnqueuePacket(data) + # return + # if packetType == PacketTypes.PT_REMOVE_SENSOR.value: + # try: + # info(PacketTypes.PT_REMOVE_SENSOR) + # retValue = False + # if 'Name' in messageObject: + # Name = messageObject['Name'] + # retValue = self.sensorsClient.RemoveSensor(Name) + # data = {} + # data['Name'] = Name + # data['PacketType'] = PacketTypes.PT_REMOVE_SENSOR.value + # data['MachineName'] = self.MachineId + # data['Response'] = retValue + # self.EnqueuePacket(data) + # except Exception as ex: + # exception("PT_REMOVE_SENSOR Unexpected error"+ str(ex)) + # retValue = False + # return + # if packetType == PacketTypes.PT_DEVICE_COMMAND.value: + # info(PacketTypes.PT_DEVICE_COMMAND) + # self.ProcessDeviceCommand(messageObject) + # return # if packetType == PacketTypes.PT_ADD_SCHEDULE.value: # info(PacketTypes.PT_ADD_SCHEDULE.value) # retVal = self.schedulerEngine.AddScheduledItem(messageObject, True) @@ -785,6 +787,12 @@ def ExecuteMessage(self, messageObject): # return info("Skipping not required packet: " + str(packetType)) + def ProcessPowerCommand(self, message): + """Process command to reboot/shutdown the system""" + commands = {'reset': 'sudo shutdown -r now', 'halt': 'sudo shutdown -h now'} + output, result = executeCommand(commands[message['payload']]) + debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output)) + def ProcessConfigCommand(self, message): """Process system configuration command""" value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable @@ -794,14 +802,12 @@ def ProcessConfigCommand(self, message): def ProcessGpioCommand(self, message): """Process GPIO command""" - info('ProcessGpioCommand: {}'.format(message)) channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', '')) result = self.sensorsClient.GpioCommand(message['suffix'], channel, message['payload']) debug('ProcessGpioCommand result: {}'.format(result)) def ProcessSensorCommand(self, message): """Process sensor command""" - info('ProcessSensorCommand: {}'.format(message)) sensor_info = message['channel'].replace(cayennemqtt.DEV_SENSOR + ':', '').split(':') sensor = sensor_info[0] channel = None @@ -810,103 +816,115 @@ def ProcessSensorCommand(self, message): result = self.sensorsClient.SensorCommand(message['suffix'], sensor, channel, message['payload']) debug('ProcessSensorCommand result: {}'.format(result)) - def ProcessDeviceCommand(self, messageObject): - """Execute a command to run on the device as specified in a message object""" - commandType = messageObject['Type'] - commandService = messageObject['Service'] - parameters = messageObject['Parameters'] - info('PT_DEVICE_COMMAND: ' + dumps(messageObject)) - debug('ProcessDeviceCommand: ' + commandType + ' ' + commandService + ' ' + str(parameters)) - id = messageObject['Id'] - sensorId = None - if 'SensorId' in messageObject: - sensorId = messageObject['SensorId'] - data = {} - retValue = '' - # if commandService == 'wifi': - # if commandType == 'status': - # retValue = self.wifiManager.GetStatus() - # if commandType == 'scan': - # retValue = self.wifiManager.GetWirelessNetworks() - # if commandType == 'setup': - # try: - # ssid = parameters["ssid"] - # password = parameters["password"] - # interface = parameters["interface"] - # retValue = self.wifiManager.Setup(ssid, password, interface) - # except: - # retValue = False - # if commandService == 'services': - # serviceName = parameters['ServiceName'] - # if commandType == 'status': - # retValue = self.serviceManager.Status(serviceName) - # if commandType == 'start': - # retValue = self.serviceManager.Start(serviceName) - # if commandType == 'stop': - # retValue = self.serviceManager.Stop(serviceName) - if commandService == 'sensor': - debug('SENSOR_COMMAND processing: ' + str(parameters)) - method = None - channel = None - value = None - driverClass = None - sensorType = None - sensorName = None - if 'SensorName' in parameters: - sensorName = parameters["SensorName"] - if 'DriverClass' in parameters: - driverClass = parameters["DriverClass"] - if commandType == 'enable': - sensor = None - enable = None - if 'Sensor' in parameters: - sensor = parameters["Sensor"] - if 'Enable' in parameters: - enable = parameters["Enable"] - retValue = self.sensorsClient.EnableSensor(sensor, enable) - else: - if commandType == 'edit': - description = sensorName - device = None - if "Description" in parameters: - description = parameters["Description"] - if "Args" in parameters: - args = parameters["Args"] - retValue = self.sensorsClient.EditSensor(sensorName, description, driverClass, args) - # else: - # if 'Channel' in parameters: - # channel = parameters["Channel"] - # if 'Method' in parameters: - # method = parameters["Method"] - # if 'Value' in parameters: - # value = parameters["Value"] - # if 'SensorType' in parameters: - # sensorType = parameters["SensorType"] - # retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) - # if commandService == 'gpio': - # method = parameters["Method"] - # channel = parameters["Channel"] - # value = parameters["Value"] - # debug('ProcessDeviceCommand: ' + commandService + ' ' + method + ' ' + str(channel) + ' ' + str(value)) - # retValue = str(self.sensorsClient.GpioCommand(commandType, method, channel, value)) - # debug('ProcessDeviceCommand gpio returned value: ' + retValue) - # if commandService == 'config': - # try: - # config_id = parameters["id"] - # arguments = parameters["arguments"] - # (retValue, output) = SystemConfig.ExecuteConfigCommand(config_id, arguments) - # data["Output"] = output - # retValue = str(retValue) - # except: - # exception("Exception on config") - data['Response'] = retValue - data['Id'] = id - data['PacketType'] = PacketTypes.PT_DEVICE_COMMAND_RESPONSE.value - data['MachineName'] = self.MachineId - info('PT_DEVICE_COMMAND_RESPONSE: ' + dumps(data)) - if sensorId: - data['SensorId'] = sensorId - self.EnqueuePacket(data) + def ProcessDeviceCommand(self, message): + """Process a device command to add/edit/remove a sensor""" + payload = message['payload'] + info('ProcessDeviceCommand payload: {}'.format(payload)) + if message['suffix'] == 'add': + result = self.sensorsClient.AddSensor(payload['id'], payload['description'], payload['class'], payload['args']) + elif message['suffix'] == 'edit': + result = self.sensorsClient.EditSensor(payload['id'], payload['description'], payload['class'], payload['args']) + elif message['suffix'] == 'delete': + result = self.sensorsClient.RemoveSensor(payload['id']) + else: + info('Unknown device command: {}'.format(message['suffix'])) + debug('ProcessDeviceCommand result: {}'.format(result)) + + # commandType = messageObject['Type'] + # commandService = messageObject['Service'] + # parameters = messageObject['Parameters'] + # info('PT_DEVICE_COMMAND: ' + dumps(messageObject)) + # debug('ProcessDeviceCommand: ' + commandType + ' ' + commandService + ' ' + str(parameters)) + # id = messageObject['Id'] + # sensorId = None + # if 'SensorId' in messageObject: + # sensorId = messageObject['SensorId'] + # data = {} + # retValue = '' + # # if commandService == 'wifi': + # # if commandType == 'status': + # # retValue = self.wifiManager.GetStatus() + # # if commandType == 'scan': + # # retValue = self.wifiManager.GetWirelessNetworks() + # # if commandType == 'setup': + # # try: + # # ssid = parameters["ssid"] + # # password = parameters["password"] + # # interface = parameters["interface"] + # # retValue = self.wifiManager.Setup(ssid, password, interface) + # # except: + # # retValue = False + # # if commandService == 'services': + # # serviceName = parameters['ServiceName'] + # # if commandType == 'status': + # # retValue = self.serviceManager.Status(serviceName) + # # if commandType == 'start': + # # retValue = self.serviceManager.Start(serviceName) + # # if commandType == 'stop': + # # retValue = self.serviceManager.Stop(serviceName) + # if commandService == 'sensor': + # debug('SENSOR_COMMAND processing: ' + str(parameters)) + # method = None + # channel = None + # value = None + # driverClass = None + # sensorType = None + # sensorName = None + # if 'SensorName' in parameters: + # sensorName = parameters["SensorName"] + # if 'DriverClass' in parameters: + # driverClass = parameters["DriverClass"] + # if commandType == 'enable': + # sensor = None + # enable = None + # if 'Sensor' in parameters: + # sensor = parameters["Sensor"] + # if 'Enable' in parameters: + # enable = parameters["Enable"] + # retValue = self.sensorsClient.EnableSensor(sensor, enable) + # else: + # if commandType == 'edit': + # description = sensorName + # device = None + # if "Description" in parameters: + # description = parameters["Description"] + # if "Args" in parameters: + # args = parameters["Args"] + # retValue = self.sensorsClient.EditSensor(sensorName, description, driverClass, args) + # # else: + # # if 'Channel' in parameters: + # # channel = parameters["Channel"] + # # if 'Method' in parameters: + # # method = parameters["Method"] + # # if 'Value' in parameters: + # # value = parameters["Value"] + # # if 'SensorType' in parameters: + # # sensorType = parameters["SensorType"] + # # retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) + # # if commandService == 'gpio': + # # method = parameters["Method"] + # # channel = parameters["Channel"] + # # value = parameters["Value"] + # # debug('ProcessDeviceCommand: ' + commandService + ' ' + method + ' ' + str(channel) + ' ' + str(value)) + # # retValue = str(self.sensorsClient.GpioCommand(commandType, method, channel, value)) + # # debug('ProcessDeviceCommand gpio returned value: ' + retValue) + # # if commandService == 'config': + # # try: + # # config_id = parameters["id"] + # # arguments = parameters["arguments"] + # # (retValue, output) = SystemConfig.ExecuteConfigCommand(config_id, arguments) + # # data["Output"] = output + # # retValue = str(retValue) + # # except: + # # exception("Exception on config") + # data['Response'] = retValue + # data['Id'] = id + # data['PacketType'] = PacketTypes.PT_DEVICE_COMMAND_RESPONSE.value + # data['MachineName'] = self.MachineId + # info('PT_DEVICE_COMMAND_RESPONSE: ' + dumps(data)) + # if sensorId: + # data['SensorId'] = sensorId + # self.EnqueuePacket(data) def EnqueuePacket(self, message): """Enqueue a message packet to send to the server""" @@ -925,13 +943,13 @@ def DequeuePacket(self): packet = None return packet - def RequestSchedules(self): - """Enqueue a packet to request schedules from the server""" - data = {} - data['MachineName'] = self.MachineId - data['Stored'] = "dynamodb" - data['PacketType'] = PacketTypes.PT_REQUEST_SCHEDULES.value - self.EnqueuePacket(data) + # def RequestSchedules(self): + # """Enqueue a packet to request schedules from the server""" + # data = {} + # data['MachineName'] = self.MachineId + # data['Stored'] = "dynamodb" + # data['PacketType'] = PacketTypes.PT_REQUEST_SCHEDULES.value + # self.EnqueuePacket(data) def SendHistoryData(self): """Enqueue a packet containing historical data to send to the server""" diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 4e042ad..46ad414 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -14,7 +14,6 @@ from myDevices.cloud.dbmanager import DbManager from myDevices.utils.threadpool import ThreadPool from hashlib import sha1 -import urllib.request as req from myDevices.devices.bus import checkAllBus, BUSLIST from myDevices.devices.digital.gpio import NativeGPIO as GPIO from myDevices.devices import manager @@ -269,7 +268,7 @@ def AddSensor(self, name, description, device, args): try: sensorAdd = {} if name: - sensorAdd['name'] = req.pathname2url(name) + sensorAdd['name'] = name if device: sensorAdd['device'] = device if args: @@ -301,7 +300,7 @@ def EditSensor(self, name, description, device, args): bVal = False try: sensorEdit = {} - name = req.pathname2url(name) + name = name sensorEdit['name'] = name sensorEdit['device'] = device sensorEdit['description'] = description @@ -327,7 +326,7 @@ def RemoveSensor(self, name): """ bVal = False try: - sensorRemove = req.pathname2url(name) + sensorRemove = name with self.sensorMutex: retValue = manager.removeDevice(sensorRemove) info('Remove device returned: {}'.format(retValue)) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index fffa8b3..c24ee21 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -55,7 +55,6 @@ def testSensorsInfo(self): self.assertEqual('dev:', sensor['channel'][:4]) self.assertIn('value', sensor) - def testSetFunction(self): self.setChannelFunction(GPIO().pins[7], 'IN') self.setChannelFunction(GPIO().pins[7], 'OUT') @@ -65,55 +64,57 @@ def testSetValue(self): self.setChannelValue(GPIO().pins[7], 1) self.setChannelValue(GPIO().pins[7], 0) - # def testSensors(self): - # #Test adding a sensor - # channel = GPIO().pins[8] - # testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} - # compareKeys = ('args', 'description', 'device') - # retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) - # self.assertTrue(retValue) - # retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == testSensor['name']) - # for key in compareKeys: - # self.assertEqual(testSensor[key], retrievedSensor[key]) - # #Test updating a sensor - # editedSensor = testSensor - # editedSensor['args']['channel'] = GPIO().pins[5] - # retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) - # self.assertTrue(retValue) - # retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) - # for key in compareKeys: - # self.assertEqual(editedSensor[key], retrievedSensor[key]) - # #Test removing a sensor - # retValue = SensorsClientTest.client.RemoveSensor(testSensor['name']) - # self.assertTrue(retValue) - # deviceNames = [device['name'] for device in SensorsClientTest.client.GetDevices()] - # self.assertNotIn(testSensor['name'], deviceNames) + def testSensors(self): + #Test adding a sensor + channel = GPIO().pins[8] + testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} + compareKeys = ('args', 'description', 'device') + retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) + self.assertTrue(retValue) + retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == testSensor['name']) + for key in compareKeys: + self.assertEqual(testSensor[key], retrievedSensor[key]) + #Test updating a sensor + editedSensor = testSensor + editedSensor['args']['channel'] = GPIO().pins[5] + retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) + self.assertTrue(retValue) + retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) + for key in compareKeys: + self.assertEqual(editedSensor[key], retrievedSensor[key]) + #Test removing a sensor + retValue = SensorsClientTest.client.RemoveSensor(testSensor['name']) + self.assertTrue(retValue) + deviceNames = [device['name'] for device in SensorsClientTest.client.GetDevices()] + self.assertNotIn(testSensor['name'], deviceNames) - # def testSensorInfo(self): - # actuator_channel = GPIO().pins[9] - # light_switch_channel = GPIO().pins[9] - # sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': actuator_channel}, 'name': 'test_actuator'}, - # 'light_switch' : {'description': 'Light Switch', 'device': 'LightSwitch', 'args': {'gpio': 'GPIO', 'invert': True, 'channel': light_switch_channel}, 'name': 'test_light_switch'}, - # # 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, - # # 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} - # } - # for sensor in sensors.values(): - # SensorsClientTest.client.AddSensor(sensor['name'], sensor['description'], sensor['device'], sensor['args']) - # SensorsClientTest.client.SensorsInfo() - # #Test setting sensor values - # self.setSensorValue(sensors['actuator'], 1) - # self.setSensorValue(sensors['actuator'], 0) - # self.setSensorValue(sensors['light_switch'], 1) - # self.setSensorValue(sensors['light_switch'], 0) - # #Test getting analog value - # # retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensors['distance']['name']) - # # self.assertEqual(retrievedSensorInfo['float'], 0.0) - # for sensor in sensors.values(): - # self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) + def testSensorInfo(self): + actuator_channel = GPIO().pins[9] + light_switch_channel = GPIO().pins[9] + sensors = {'actuator' : {'description': 'Digital Output', 'device': 'DigitalActuator', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': actuator_channel}, 'name': 'test_actuator'}, + 'light_switch' : {'description': 'Light Switch', 'device': 'LightSwitch', 'args': {'gpio': 'GPIO', 'invert': True, 'channel': light_switch_channel}, 'name': 'test_light_switch'}, + 'MCP3004' : {'description': 'MCP3004', 'device': 'MCP3004', 'args': {'chip': '0'}, 'name': 'test_MCP3004'}, + 'distance' : {'description': 'Analog Distance Sensor', 'device': 'DistanceSensor', 'args': {'adc': 'test_MCP3004', 'channel': 0}, 'name': 'test_distance'} + } + for sensor in sensors.values(): + SensorsClientTest.client.AddSensor(sensor['name'], sensor['description'], sensor['device'], sensor['args']) + SensorsClientTest.client.SensorsInfo() + #Test setting sensor values + self.setSensorValue(sensors['actuator'], 1) + self.setSensorValue(sensors['actuator'], 0) + self.setSensorValue(sensors['light_switch'], 1) + self.setSensorValue(sensors['light_switch'], 0) + #Test getting analog value + channel = 'dev:{}'.format(SensorsClientTest.client.SHA_Calc_str(sensors['distance']['name']+sensors['distance']['device'])) + retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['channel'] == channel) + self.assertEqual(retrievedSensorInfo['value'], 0.0) + for sensor in sensors.values(): + self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) def setSensorValue(self, sensor, value): - SensorsClientTest.client.SensorCommand('integer', sensor['name'], sensor['device'], None, None, None, value) - sensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['name'] == sensor['name']) + SensorsClientTest.client.SensorCommand('integer', sensor['name'], None, value) + channel = 'dev:{}'.format(SensorsClientTest.client.SHA_Calc_str(sensor['name']+sensor['device'])) + sensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['channel'] == channel) self.assertEqual(value, sensorInfo['value']) def setChannelFunction(self, channel, function): From f392b9358d2f2a15e9cfd419b6aaa6673a831529 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 17 Oct 2017 16:14:56 -0600 Subject: [PATCH 076/129] Process uninstall using data channel, remove unneeded packet handling. --- myDevices/cloud/cayennemqtt.py | 1 + myDevices/cloud/client.py | 34 +++++++++++++++++++--------------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 9356aa8..5fdc376 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -26,6 +26,7 @@ SYS_POWER = 'sys:pwr' AGENT_VERSION = 'agent:version' AGENT_DEVICES = 'agent:devices' +AGENT_UNINSTALL = 'agent:uninstall' DEV_SENSOR = 'dev' # Channel Suffixes diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 1a09e5a..e4de155 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -42,8 +42,8 @@ class PacketTypes(Enum): # PT_SYSTEM_INFO = 4 # PT_PROCESS_LIST = 5 # PT_STARTUP_APPLICATIONS = 8 - PT_START_RDS = 11 - PT_STOP_RDS = 12 + # PT_START_RDS = 11 + # PT_STOP_RDS = 12 # PT_RESTART_COMPUTER = 25 # PT_SHUTDOWN_COMPUTER = 26 # PT_KILL_PROCESS = 27 @@ -51,7 +51,7 @@ class PacketTypes(Enum): # PT_UPDATE_SCHEDULES = 41 # PT_AGENT_MESSAGE = 45 # PT_PRODUCT_INFO = 50 - PT_UNINSTALL_AGENT = 51 + # PT_UNINSTALL_AGENT = 51 # PT_ADD_SENSOR = 61 # PT_REMOVE_SENSOR = 62 # PT_UPDATE_SENSORPT_UPDATE_SENSOR = 63 @@ -60,11 +60,11 @@ class PacketTypes(Enum): # PT_ADD_SCHEDULE = 66 # PT_REMOVE_SCHEDULE = 67 # PT_GET_SCHEDULES = 68 - PT_NOTIFICATION = 69 + # PT_NOTIFICATION = 69 # PT_DATA_CHANGED = 70 # PT_HISTORY_DATA = 71 # PT_HISTORY_DATA_RESPONSE = 72 - PT_AGENT_CONFIGURATION = 74 + # PT_AGENT_CONFIGURATION = 74 def GetTime(): @@ -617,10 +617,14 @@ def ExecuteMessage(self, message): self.ProcessDeviceCommand(message) elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): self.ProcessConfigCommand(message) + elif channel == cayennemqtt.AGENT_UNINSTALL: + executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh') + # elif channel == cayennemqtt.AGENT_CONFIG: + # self.config.setCloudConfig(message['payload']) else: info('Unknown message') - packetType = int(message['PacketType']) + # packetType = int(message['PacketType']) # if packetType == PacketTypes.PT_UTILIZATION.value: # self.SendSystemUtilization() # info(PacketTypes.PT_UTILIZATION) @@ -630,10 +634,10 @@ def ExecuteMessage(self, message): # self.SendSystemState() # info(PacketTypes.PT_SYSTEM_INFO) # return - if packetType == PacketTypes.PT_UNINSTALL_AGENT.value: - command = "sudo /etc/myDevices/uninstall/uninstall.sh" - executeCommand(command) - return + # if packetType == PacketTypes.PT_UNINSTALL_AGENT.value: + # command = "sudo /etc/myDevices/uninstall/uninstall.sh" + # executeCommand(command) + # return # if packetType == PacketTypes.PT_STARTUP_APPLICATIONS.value: # self.BuildPT_STARTUP_APPLICATIONS() # info(PacketTypes.PT_STARTUP_APPLICATIONS) @@ -670,10 +674,10 @@ def ExecuteMessage(self, message): # command = "sudo shutdown -h now" # executeCommand(command) # return - if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: - info('PT_AGENT_CONFIGURATION: ' + str(message.Data)) - self.config.setCloudConfig(message.Data) - return + # if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: + # info('PT_AGENT_CONFIGURATION: ' + str(message.Data)) + # self.config.setCloudConfig(message.Data) + # return # if packetType == PacketTypes.PT_ADD_SENSOR.value: # try: # info(PacketTypes.PT_ADD_SENSOR) @@ -785,7 +789,7 @@ def ExecuteMessage(self, message): # except: # exception('Processing history response packet failed') # return - info("Skipping not required packet: " + str(packetType)) + # info("Skipping not required packet: " + str(packetType)) def ProcessPowerCommand(self, message): """Process command to reboot/shutdown the system""" From 165c0581ef1d119a6bd5f14edbc2a126d1d0eec0 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 18 Oct 2017 15:19:52 -0600 Subject: [PATCH 077/129] Use sensor name and type to identify sensors, instead of a hash. --- myDevices/sensors/sensors.py | 167 +++++++++++++++++---------------- myDevices/test/sensors_test.py | 11 ++- 2 files changed, 91 insertions(+), 87 deletions(-) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 46ad414..b29b19d 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -9,7 +9,6 @@ from myDevices.system import services from datetime import datetime, timedelta from os import path, getpid -from urllib import parse from myDevices.utils.daemon import Daemon from myDevices.cloud.dbmanager import DbManager from myDevices.utils.threadpool import ThreadPool @@ -122,61 +121,60 @@ def SystemInformation(self): exception('SystemInformation failed') return newSystemInfo - def SHA_Calc(self, object): - """Return SHA value for an object""" - if object == None: - return '' - try: - strVal = dumps(object) - except: - exception('SHA_Calc failed for:' + str(object)) - return '' - return self.SHA_Calc_str(strVal) - - def SHA_Calc_str(self, stringVal): - """Return SHA value for a string""" - m = sha1() - m.update(stringVal.encode('utf8')) - sDigest = str(m.hexdigest()) - return sDigest - - def AppendToDeviceList(self, device_list, source, device_type): - """Append a sensor/actuator device to device list - - Args: - device_list: Device list to append device to - source: Device to append to list - device_type: Type of device - """ - device = source.copy() - del device['origin'] - device['name'] = parse.unquote(device['name']) - device['type'] = device_type - if len(source['type']) > 1: - device['hash'] = self.SHA_Calc_str(device['name']+device['type']) - else: - device['hash'] = self.SHA_Calc_str(device['name']+device['device']) - if device['hash'] in self.disabledSensors: - device['enabled'] = 0 - else: - device['enabled'] = 1 - device_list.append(device) - - def GetDevices(self): - """Return a list of current sensor/actuator devices""" - manager.deviceDetector() - device_list = manager.getDeviceList() - devices = [] - for dev in device_list: - try: - if len(dev['type']) == 0: - self.AppendToDeviceList(devices, dev, '') - else: - for device_type in dev['type']: - self.AppendToDeviceList(devices, dev, device_type) - except: - exception("Failed to get device: {}".format(dev)) - return devices + # def SHA_Calc(self, object): + # """Return SHA value for an object""" + # if object == None: + # return '' + # try: + # strVal = dumps(object) + # except: + # exception('SHA_Calc failed for:' + str(object)) + # return '' + # return self.SHA_Calc_str(strVal) + + # def SHA_Calc_str(self, stringVal): + # """Return SHA value for a string""" + # m = sha1() + # m.update(stringVal.encode('utf8')) + # sDigest = str(m.hexdigest()) + # return sDigest + + # def AppendToDeviceList(self, device_list, source, device_type): + # """Append a sensor/actuator device to device list + + # Args: + # device_list: Device list to append device to + # source: Device to append to list + # device_type: Type of device + # """ + # device = source.copy() + # del device['origin'] + # device['type'] = device_type + # if len(source['type']) > 1: + # device['hash'] = self.SHA_Calc_str(device['name']+device['type']) + # else: + # device['hash'] = self.SHA_Calc_str(device['name']+device['device']) + # if device['hash'] in self.disabledSensors: + # device['enabled'] = 0 + # else: + # device['enabled'] = 1 + # device_list.append(device) + + # def GetDevices(self): + # """Return a list of current sensor/actuator devices""" + # manager.deviceDetector() + # device_list = manager.getDeviceList() + # devices = [] + # for dev in device_list: + # try: + # if len(dev['type']) == 0: + # self.AppendToDeviceList(devices, dev, '') + # else: + # for device_type in dev['type']: + # self.AppendToDeviceList(devices, dev, device_type) + # except: + # exception("Failed to get device: {}".format(dev)) + # return devices def CallDeviceFunction(self, func, *args): """Call a function for a sensor/actuator device and format the result value type @@ -211,7 +209,7 @@ def BusInfo(self): def SensorsInfo(self): """Return a list with current sensor states for all enabled sensors""" - devices = self.GetDevices() + devices = manager.getDeviceList() sensors_info = [] if devices is None: return sensors_info @@ -219,35 +217,40 @@ def SensorsInfo(self): sensor = instance.deviceInstance(device['name']) if 'enabled' not in device or device['enabled'] == 1: sensor_types = {'Temperature': {'function': 'getCelsius', 'data_args': {'type': 'temp', 'unit': 'c'}}, - 'Humidity': {'function': 'getHumidityPercent', 'data_args': {'type': 'rel_hum', 'unit': 'p'}}, - 'Pressure': {'function': 'getPascal', 'data_args': {'type': 'bp', 'unit': 'pa'}}, - 'Luminosity': {'function': 'getLux', 'data_args': {'type': 'lum', 'unit': 'lux'}}, - 'Distance': {'function': 'getCentimeter', 'data_args': {'type': 'prox', 'unit': 'cm'}}, - 'ServoMotor': {'function': 'readAngle', 'data_args': {'type': 'analog_actuator'}}, - 'DigitalSensor': {'function': 'read', 'data_args': {'type': 'digital_sensor', 'unit': 'd'}}, - 'DigitalActuator': {'function': 'read', 'data_args': {'type': 'digital_actuator', 'unit': 'd'}}, - 'AnalogSensor': {'function': 'readFloat', 'data_args': {'type': 'analog_sensor'}}, - 'AnalogActuator': {'function': 'readFloat', 'data_args': {'type': 'analog_actuator'}}} + 'Humidity': {'function': 'getHumidityPercent', 'data_args': {'type': 'rel_hum', 'unit': 'p'}}, + 'Pressure': {'function': 'getPascal', 'data_args': {'type': 'bp', 'unit': 'pa'}}, + 'Luminosity': {'function': 'getLux', 'data_args': {'type': 'lum', 'unit': 'lux'}}, + 'Distance': {'function': 'getCentimeter', 'data_args': {'type': 'prox', 'unit': 'cm'}}, + 'ServoMotor': {'function': 'readAngle', 'data_args': {'type': 'analog_actuator'}}, + 'DigitalSensor': {'function': 'read', 'data_args': {'type': 'digital_sensor', 'unit': 'd'}}, + 'DigitalActuator': {'function': 'read', 'data_args': {'type': 'digital_actuator', 'unit': 'd'}}, + 'AnalogSensor': {'function': 'readFloat', 'data_args': {'type': 'analog_sensor'}}, + 'AnalogActuator': {'function': 'readFloat', 'data_args': {'type': 'analog_actuator'}}} extension_types = {'ADC': {'function': 'analogReadAllFloat'}, 'DAC': {'function': 'analogReadAllFloat'}, 'PWM': {'function': 'pwmWildcard'}, 'DAC': {'function': 'wildcard'}} - if device['type'] in sensor_types: - try: - sensor_type = sensor_types[device['type']] - func = getattr(sensor, sensor_type['function']) - cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['hash'], value=self.CallDeviceFunction(func), **sensor_type['data_args']) - except: - exception('Failed to get sensor data: {} {}'.format(device['type'], device['name'])) - else: - try: - extension_type = extension_types[device['type']] - func = getattr(sensor, extension_type['function']) - values = self.CallDeviceFunction(func) - for pin, value in values.items(): - cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['hash'] + ':' + str(pin), cayennemqtt.VALUE, value) - except: - exception('Failed to get extension data: {} {}'.format(device['type'], device['name'])) + for device_type in device['type']: + if device_type in sensor_types: + try: + sensor_type = sensor_types[device_type] + func = getattr(sensor, sensor_type['function']) + if len(device['type']) > 1: + channel = '{}:{}'.format(device['name'], device_type.lower()) + else: + channel = device['name'] + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, channel, value=self.CallDeviceFunction(func), **sensor_type['data_args']) + except: + exception('Failed to get sensor data: {} {}'.format(device_type, device['name'])) + else: + try: + extension_type = extension_types[device_type] + func = getattr(sensor, extension_type['function']) + values = self.CallDeviceFunction(func) + for pin, value in values.items(): + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['name'] + ':' + str(pin), cayennemqtt.VALUE, value) + except: + exception('Failed to get extension data: {} {}'.format(device_type, device['name'])) logJson('Sensors info: {}'.format(sensors_info)) return sensors_info diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index c24ee21..3258a55 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -68,10 +68,11 @@ def testSensors(self): #Test adding a sensor channel = GPIO().pins[8] testSensor = {'description': 'Digital Input', 'device': 'DigitalSensor', 'args': {'gpio': 'GPIO', 'invert': False, 'channel': channel}, 'name': 'testdevice'} + SensorsClientTest.client.RemoveSensor(testSensor['name']) #Attempt to remove device if it already exists from a previous test compareKeys = ('args', 'description', 'device') retValue = SensorsClientTest.client.AddSensor(testSensor['name'], testSensor['description'], testSensor['device'], testSensor['args']) self.assertTrue(retValue) - retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == testSensor['name']) + retrievedSensor = next(obj for obj in manager.getDeviceList() if obj['name'] == testSensor['name']) for key in compareKeys: self.assertEqual(testSensor[key], retrievedSensor[key]) #Test updating a sensor @@ -79,13 +80,13 @@ def testSensors(self): editedSensor['args']['channel'] = GPIO().pins[5] retValue = SensorsClientTest.client.EditSensor(editedSensor['name'], editedSensor['description'], editedSensor['device'], editedSensor['args']) self.assertTrue(retValue) - retrievedSensor = next(obj for obj in SensorsClientTest.client.GetDevices() if obj['name'] == editedSensor['name']) + retrievedSensor = next(obj for obj in manager.getDeviceList() if obj['name'] == editedSensor['name']) for key in compareKeys: self.assertEqual(editedSensor[key], retrievedSensor[key]) #Test removing a sensor retValue = SensorsClientTest.client.RemoveSensor(testSensor['name']) self.assertTrue(retValue) - deviceNames = [device['name'] for device in SensorsClientTest.client.GetDevices()] + deviceNames = [device['name'] for device in manager.getDeviceList()] self.assertNotIn(testSensor['name'], deviceNames) def testSensorInfo(self): @@ -105,7 +106,7 @@ def testSensorInfo(self): self.setSensorValue(sensors['light_switch'], 1) self.setSensorValue(sensors['light_switch'], 0) #Test getting analog value - channel = 'dev:{}'.format(SensorsClientTest.client.SHA_Calc_str(sensors['distance']['name']+sensors['distance']['device'])) + channel = 'dev:{}'.format(sensors['distance']['name']) retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['channel'] == channel) self.assertEqual(retrievedSensorInfo['value'], 0.0) for sensor in sensors.values(): @@ -113,7 +114,7 @@ def testSensorInfo(self): def setSensorValue(self, sensor, value): SensorsClientTest.client.SensorCommand('integer', sensor['name'], None, value) - channel = 'dev:{}'.format(SensorsClientTest.client.SHA_Calc_str(sensor['name']+sensor['device'])) + channel = 'dev:{}'.format(sensor['name']) sensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['channel'] == channel) self.assertEqual(value, sensorInfo['value']) From a6015ec6a0bb400be7c35c3c984bb0a6d149dcf5 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 19 Oct 2017 16:21:59 -0600 Subject: [PATCH 078/129] Remove unused code. --- myDevices/cloud/client.py | 499 +------------------------------ myDevices/cloud/scheduler.py | 2 +- myDevices/sensors/sensors.py | 56 ---- myDevices/system/systemconfig.py | 12 - myDevices/system/systeminfo.py | 23 -- 5 files changed, 17 insertions(+), 575 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index e4de155..a33fecf 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -10,7 +10,6 @@ from threading import Thread, RLock from time import strftime, localtime, tzset, time, sleep from queue import Queue, Empty -from enum import Enum, unique from myDevices.utils.config import Config from myDevices.utils.logger import exception, info, warn, error, debug, logJson from myDevices.system import services, ipgetter @@ -35,38 +34,6 @@ GENERAL_SLEEP_THREAD = 0.20 -@unique -class PacketTypes(Enum): - """Packet types used when sending/receiving messages""" - # PT_UTILIZATION = 3 - # PT_SYSTEM_INFO = 4 - # PT_PROCESS_LIST = 5 - # PT_STARTUP_APPLICATIONS = 8 - # PT_START_RDS = 11 - # PT_STOP_RDS = 12 - # PT_RESTART_COMPUTER = 25 - # PT_SHUTDOWN_COMPUTER = 26 - # PT_KILL_PROCESS = 27 - # PT_REQUEST_SCHEDULES = 40 - # PT_UPDATE_SCHEDULES = 41 - # PT_AGENT_MESSAGE = 45 - # PT_PRODUCT_INFO = 50 - # PT_UNINSTALL_AGENT = 51 - # PT_ADD_SENSOR = 61 - # PT_REMOVE_SENSOR = 62 - # PT_UPDATE_SENSORPT_UPDATE_SENSOR = 63 - # PT_DEVICE_COMMAND = 64 - # PT_DEVICE_COMMAND_RESPONSE = 65 - # PT_ADD_SCHEDULE = 66 - # PT_REMOVE_SCHEDULE = 67 - # PT_GET_SCHEDULES = 68 - # PT_NOTIFICATION = 69 - # PT_DATA_CHANGED = 70 - # PT_HISTORY_DATA = 71 - # PT_HISTORY_DATA_RESPONSE = 72 - # PT_AGENT_CONFIGURATION = 74 - - def GetTime(): """Return string with the current time""" tzset() @@ -246,10 +213,8 @@ def __init__(self, host, port, cayenneApiHost): self.password = None self.clientId = None self.CheckSubscription() - #self.defaultRDServer = self.networkConfig.get('CONFIG','RemoteDesktopServerAddress') self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.Initialize() - # self.FirstRun() self.updater = Updater(self.config) self.updater.start() self.initialized = True @@ -271,14 +236,13 @@ def Initialize(self): self.downloadSpeed.getDownloadSpeed() self.connected = False self.exiting = False - self.Start + self.Start() self.count = 10000 self.buff = bytearray(self.count) #start thread only after init of other fields self.sensorsClient.SetDataChanged(self.OnDataChanged) self.processManager = services.ProcessManager() self.serviceManager = services.ServiceManager() - # self.wifiManager = WifiManager.WifiManager() self.writerThread = WriterThread('writer', self) self.writerThread.start() self.processorThread = ProcessorThread('processor', self) @@ -311,187 +275,45 @@ def Destroy(self): self.Stop() info('Client shut down') - # def FirstRun(self): - # """Send messages when client is first started""" - # self.SendSystemInfo() - def OnDataChanged(self, data): """Enqueue a packet containing changed system data to send to the server""" self.EnqueuePacket(data) - # data = {} - # data['MachineName'] = self.MachineId - # data['PacketType'] = PacketTypes.PT_DATA_CHANGED.value - # data['Timestamp'] = int(time()) - # data['RaspberryInfo'] = systemData - # self.EnqueuePacket(data) - # del data - # del systemData def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" try: - # debug('SendSystemInfo') - data_list = [] - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_HARDWARE_MAKE, value=self.hardware.getManufacturer()) - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_HARDWARE_MODEL, value=self.hardware.getModel()) - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) - cayennemqtt.DataChannel.add(data_list, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent','Version')) + debug('SendSystemInfo') + data = [] + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MAKE, value=self.hardware.getManufacturer()) + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MODEL, value=self.hardware.getModel()) + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) + cayennemqtt.DataChannel.add(data, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent','Version')) config = SystemConfig.getConfig() if config: channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} for key, channel in channel_map.items(): try: - cayennemqtt.DataChannel.add(data_list, channel, value=config[key]) + cayennemqtt.DataChannel.add(data, channel, value=config[key]) except: pass - self.EnqueuePacket(data_list) - # data = {} - # data['MachineName'] = self.MachineId - # data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value - # data['IpAddress'] = self.PublicIP - # data['GatewayMACAddress'] = self.hardware.getMac() - # systemData = {} - # # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - # # systemData['AntiVirus'] = 'None' - # # systemData['Firewall'] = 'iptables' - # # systemData['FirewallEnabled'] = 'true' - # systemData['ComputerMake'] = self.hardware.getManufacturer() - # systemData['ComputerModel'] = self.hardware.getModel() - # systemData['OsName'] = self.oSInfo.ID - # # systemData['OsBuild'] = self.oSInfo.ID_LIKE - # # systemData['OsArchitecture'] = self.hardware.Revision - # systemData['OsVersion'] = self.oSInfo.VERSION_ID - # systemData['ComputerName'] = self.machineName - # systemData['AgentVersion'] = self.config.get('Agent','Version') - # systemData['GatewayMACAddress'] = self.hardware.getMac() - # systemData['OsSettings'] = SystemConfig.getConfig() - # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() - # systemData['WifiStatus'] = self.wifiManager.GetStatus() - # data['RaspberryInfo'] = systemData - # if data != self.previousSystemInfo: - # self.previousSystemInfo = data.copy() - # data['Timestamp'] = int(time()) - # self.EnqueuePacket(data) - # logJson('SendSystemInfo: ' + dumps(data), 'SendSystemInfo') - # del systemData - # del data - # data=None + self.EnqueuePacket(data) except Exception: exception('SendSystemInfo unexpected error') - # def SendSystemUtilization(self): - # """Enqueue a packet containing system utilization data to send to the server""" - # data = {} - # data['MachineName'] = self.MachineId - # data['Timestamp'] = int(time()) - # data['PacketType'] = PacketTypes.PT_UTILIZATION.value - # self.processManager.RefreshProcessManager() - # data['VisibleMemory'] = self.processManager.VisibleMemory - # data['AvailableMemory'] = self.processManager.AvailableMemory - # data['AverageProcessorUsage'] = self.processManager.AverageProcessorUsage - # data['PeakProcessorUsage'] = self.processManager.PeakProcessorUsage - # data['AverageMemoryUsage'] = self.processManager.AverageMemoryUsage - # data['PeakMemoryUsage'] = self.processManager.AverageMemoryUsage - # data['PercentProcessorTime'] = self.processManager.PercentProcessorTime - # self.EnqueuePacket(data) - def SendSystemState(self): """Enqueue a packet containing system information to send to the server""" try: - # debug('SendSystemState') - # self.SendSystemInfo() - # self.SendSystemUtilization() - data_list = [] + debug('SendSystemState') + data = [] download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: - cayennemqtt.DataChannel.add(data_list, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) - data_list += self.sensorsClient.systemData - self.EnqueuePacket(data_list) - # data = {} - # data['MachineName'] = self.MachineId - # data['PacketType'] = PacketTypes.PT_SYSTEM_INFO.value - # data['Timestamp'] = int(time()) - # data['IpAddress'] = self.PublicIP - # data['GatewayMACAddress'] = self.hardware.getMac() - # systemData = {} - # systemData['NetworkSpeed'] = str(self.downloadSpeed.getDownloadSpeed()) - # systemData['AntiVirus'] = 'None' - # systemData['Firewall'] = 'iptables' - # systemData['FirewallEnabled'] = 'true' - # systemData['ComputerMake'] = self.hardware.getManufacturer() - # systemData['ComputerModel'] = self.hardware.getModel() - # systemData['OsName'] = self.oSInfo.ID - # systemData['OsBuild'] = self.oSInfo.ID_LIKE if hasattr(self.oSInfo, 'ID_LIKE') else self.oSInfo.ID - # systemData['OsArchitecture'] = self.hardware.Revision - # systemData['OsVersion'] = self.oSInfo.VERSION_ID - # systemData['ComputerName'] = self.machineName - # systemData['AgentVersion'] = self.config.get('Agent', 'Version', fallback='1.0.1.0') - # systemData['InstallDate'] = self.installDate - # systemData['GatewayMACAddress'] = self.hardware.getMac() - # with self.sensorsClient.sensorMutex: - # systemData['SystemInfo'] = self.sensorsClient.currentSystemInfo - # systemData['SensorsInfo'] = self.sensorsClient.currentSensorsInfo - # systemData['BusInfo'] = self.sensorsClient.currentBusInfo - # systemData['OsSettings'] = SystemConfig.getConfig() - # systemData['NetworkId'] = WifiManager.Network.GetNetworkId() - # systemData['WifiStatus'] = self.wifiManager.GetStatus() - # try: - # history = History() - # history.SaveAverages(systemData) - # except: - # exception('History error') - # data['RaspberryInfo'] = systemData - # self.EnqueuePacket(data) - # logJson('PT_SYSTEM_INFO: ' + dumps(data), 'PT_SYSTEM_INFO') - # del systemData - # del data - # data = None + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) + data += self.sensorsClient.systemData + self.EnqueuePacket(data) except Exception as e: exception('ThreadSystemInfo unexpected error: ' + str(e)) - # def BuildPT_STARTUP_APPLICATIONS(self): - # """Schedule a function to run for retrieving a list of services""" - # ThreadPool.Submit(self.ThreadServiceManager) - - # def ThreadServiceManager(self): - # """Enqueue a packet containing a list of services to send to the server""" - # self.serviceManager.Run() - # sleep(GENERAL_SLEEP_THREAD) - # data = {} - # data['MachineName'] = self.MachineId - # data['PacketType'] = PacketTypes.PT_STARTUP_APPLICATIONS.value - # data['ProcessList'] = self.serviceManager.GetServiceList() - # self.EnqueuePacket(data) - - # def BuildPT_PROCESS_LIST(self): - # """Schedule a function to run for retrieving a list of processes""" - # ThreadPool.Submit(self.ThreadProcessManager) - - # def ThreadProcessManager(self): - # """Enqueue a packet containing a list of processes to send to the server""" - # self.processManager.Run() - # sleep(GENERAL_SLEEP_THREAD) - # data = {} - # data['MachineName'] = self.MachineId - # data['PacketType'] = PacketTypes.PT_PROCESS_LIST.value - # data['ProcessList'] = self.processManager.GetProcessList() - # self.EnqueuePacket(data) - - # def ProcessPT_KILL_PROCESS(self, message): - # """Kill a process specified in message""" - # pid = message['Pid'] - # retVal = self.processManager.KillProcess(int(pid)) - # data = {} - # data['MachineName'] = self.MachineId - # data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value - # data['Type'] = 'Info' - # if retVal: - # data['Message'] = 'Process Killed!' - # else: - # data['Message'] = 'Process not Killed!' - # self.EnqueuePacket(data) - def CheckSubscription(self): """Check that an invite code is valid""" inviteCode = self.config.get('Agent', 'InviteCode') @@ -514,7 +336,6 @@ def CheckSubscription(self): Daemon.Exit() info('CheckSubscription: MachineId {}'.format(self.MachineId)) - @property def Start(self): """Connect to the server""" started = False @@ -548,7 +369,7 @@ def Restart(self): debug('Restarting cycle...') sleep(1) self.Stop() - self.Start + self.Start() def CheckJson(self, message): """Check if a JSON message is valid""" @@ -574,23 +395,6 @@ def RunAction(self, action): return self.ExecuteMessage(action) - def SendNotification(self, notify, subject, body): - """Enqueue a notification message packet to send to the server""" - info('SendNotification: ' + str(notify) + ' ' + str(subject) + ' ' + str(body)) - return False - # try: - # data = {} - # data['PacketType'] = PacketTypes.PT_NOTIFICATION.value - # data['MachineName'] = self.MachineId - # data['Subject'] = subject - # data['Body'] = body - # data['Notify'] = notify - # self.EnqueuePacket(data) - # except: - # debug('') - # return False - # return True - def ProcessMessage(self): """Process a message from the server""" try: @@ -624,173 +428,6 @@ def ExecuteMessage(self, message): else: info('Unknown message') - # packetType = int(message['PacketType']) - # if packetType == PacketTypes.PT_UTILIZATION.value: - # self.SendSystemUtilization() - # info(PacketTypes.PT_UTILIZATION) - # return - # if packetType == PacketTypes.PT_SYSTEM_INFO.value: - # info("ExecuteMessage - sysinfo - Calling SendSystemState") - # self.SendSystemState() - # info(PacketTypes.PT_SYSTEM_INFO) - # return - # if packetType == PacketTypes.PT_UNINSTALL_AGENT.value: - # command = "sudo /etc/myDevices/uninstall/uninstall.sh" - # executeCommand(command) - # return - # if packetType == PacketTypes.PT_STARTUP_APPLICATIONS.value: - # self.BuildPT_STARTUP_APPLICATIONS() - # info(PacketTypes.PT_STARTUP_APPLICATIONS) - # return - # if packetType == PacketTypes.PT_PROCESS_LIST.value: - # self.BuildPT_PROCESS_LIST() - # info(PacketTypes.PT_PROCESS_LIST) - # return - # if packetType == PacketTypes.PT_KILL_PROCESS.value: - # self.ProcessPT_KILL_PROCESS(messageObject) - # info(PacketTypes.PT_KILL_PROCESS) - # return - # if packetType == PacketTypes.PT_PRODUCT_INFO.value: - # self.config.set('Subscription', 'ProductCode', messageObject['ProductCode']) - # info(PacketTypes.PT_PRODUCT_INFO) - # return - # if packetType == PacketTypes.PT_RESTART_COMPUTER.value: - # info(PacketTypes.PT_RESTART_COMPUTER) - # data = {} - # data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value - # data['MachineName'] = self.MachineId - # data['Message'] = 'Computer Restarted!' - # self.EnqueuePacket(data) - # command = "sudo shutdown -r now" - # executeCommand(command) - # return - # if packetType == PacketTypes.PT_SHUTDOWN_COMPUTER.value: - # info(PacketTypes.PT_SHUTDOWN_COMPUTER) - # data = {} - # data['PacketType'] = PacketTypes.PT_AGENT_MESSAGE.value - # data['MachineName'] = self.MachineId - # data['Message'] = 'Computer Powered Off!' - # self.EnqueuePacket(data) - # command = "sudo shutdown -h now" - # executeCommand(command) - # return - # if packetType == PacketTypes.PT_AGENT_CONFIGURATION.value: - # info('PT_AGENT_CONFIGURATION: ' + str(message.Data)) - # self.config.setCloudConfig(message.Data) - # return - # if packetType == PacketTypes.PT_ADD_SENSOR.value: - # try: - # info(PacketTypes.PT_ADD_SENSOR) - # parameters = None - # deviceName = None - # deviceClass = None - # description = None - # #for backward compatibility check the DisplayName and overwrite it over the other variables - # displayName = None - # if 'DisplayName' in messageObject: - # displayName = messageObject['DisplayName'] - - # if 'Parameters' in messageObject: - # parameters = messageObject['Parameters'] - - # if 'DeviceName' in messageObject: - # deviceName = messageObject['DeviceName'] - # else: - # deviceName = displayName - - # if 'Description' in messageObject: - # description = messageObject['Description'] - # else: - # description = deviceName - - # if 'Class' in messageObject: - # deviceClass = messageObject['Class'] - - # retValue = True - # retValue = self.sensorsClient.AddSensor(deviceName, description, deviceClass, parameters) - # except Exception as ex: - # exception("PT_ADD_SENSOR Unexpected error"+ str(ex)) - # retValue = False - # data = {} - # if 'Id' in messageObject: - # data['Id'] = messageObject['Id'] - # #0 - None, 1 - Pending, 2-Success, 3 - Not responding, 4 - Failure - # if retValue: - # data['State'] = 2 - # else: - # data['State'] = 4 - # data['PacketType'] = PacketTypes.PT_UPDATE_SENSOR.value - # data['MachineName'] = self.MachineId - # self.EnqueuePacket(data) - # return - # if packetType == PacketTypes.PT_REMOVE_SENSOR.value: - # try: - # info(PacketTypes.PT_REMOVE_SENSOR) - # retValue = False - # if 'Name' in messageObject: - # Name = messageObject['Name'] - # retValue = self.sensorsClient.RemoveSensor(Name) - # data = {} - # data['Name'] = Name - # data['PacketType'] = PacketTypes.PT_REMOVE_SENSOR.value - # data['MachineName'] = self.MachineId - # data['Response'] = retValue - # self.EnqueuePacket(data) - # except Exception as ex: - # exception("PT_REMOVE_SENSOR Unexpected error"+ str(ex)) - # retValue = False - # return - # if packetType == PacketTypes.PT_DEVICE_COMMAND.value: - # info(PacketTypes.PT_DEVICE_COMMAND) - # self.ProcessDeviceCommand(messageObject) - # return - # if packetType == PacketTypes.PT_ADD_SCHEDULE.value: - # info(PacketTypes.PT_ADD_SCHEDULE.value) - # retVal = self.schedulerEngine.AddScheduledItem(messageObject, True) - # if 'Update' in messageObject: - # messageObject['Update'] = messageObject['Update'] - # messageObject['PacketType'] = PacketTypes.PT_ADD_SCHEDULE.value - # messageObject['MachineName'] = self.MachineId - # messageObject['Status'] = str(retVal) - # self.EnqueuePacket(messageObject) - # return - # if packetType == PacketTypes.PT_REMOVE_SCHEDULE.value: - # info(PacketTypes.PT_REMOVE_SCHEDULE) - # retVal = self.schedulerEngine.RemoveScheduledItem(messageObject) - # messageObject['PacketType'] = PacketTypes.PT_REMOVE_SCHEDULE.value - # messageObject['MachineName'] = self.MachineId - # messageObject['Status'] = str(retVal) - # self.EnqueuePacket(messageObject) - # return - # if packetType == PacketTypes.PT_GET_SCHEDULES.value: - # info(PacketTypes.PT_GET_SCHEDULES) - # schedulesJson = self.schedulerEngine.GetSchedules() - # data['Schedules'] = schedulesJson - # data['PacketType'] = PacketTypes.PT_GET_SCHEDULES.value - # data['MachineName'] = self.MachineId - # self.EnqueuePacket(data) - # return - # if packetType == PacketTypes.PT_UPDATE_SCHEDULES.value: - # info(PacketTypes.PT_UPDATE_SCHEDULES) - # retVal = self.schedulerEngine.UpdateSchedules(messageObject) - # return - # if packetType == PacketTypes.PT_HISTORY_DATA_RESPONSE.value: - # info(PacketTypes.PT_HISTORY_DATA_RESPONSE) - # try: - # id = messageObject['Id'] - # history = History() - # if messageObject['Status']: - # history.Sent(True, self.sentHistoryData[id]['HistoryData']) - # self.historySendFails = 0 - # else: - # history.Sent(False, self.sentHistoryData[id]['HistoryData']) - # self.historySendFails += 1 - # del self.sentHistoryData[id] - # except: - # exception('Processing history response packet failed') - # return - # info("Skipping not required packet: " + str(packetType)) - def ProcessPowerCommand(self, message): """Process command to reboot/shutdown the system""" commands = {'reset': 'sudo shutdown -r now', 'halt': 'sudo shutdown -h now'} @@ -834,102 +471,6 @@ def ProcessDeviceCommand(self, message): info('Unknown device command: {}'.format(message['suffix'])) debug('ProcessDeviceCommand result: {}'.format(result)) - # commandType = messageObject['Type'] - # commandService = messageObject['Service'] - # parameters = messageObject['Parameters'] - # info('PT_DEVICE_COMMAND: ' + dumps(messageObject)) - # debug('ProcessDeviceCommand: ' + commandType + ' ' + commandService + ' ' + str(parameters)) - # id = messageObject['Id'] - # sensorId = None - # if 'SensorId' in messageObject: - # sensorId = messageObject['SensorId'] - # data = {} - # retValue = '' - # # if commandService == 'wifi': - # # if commandType == 'status': - # # retValue = self.wifiManager.GetStatus() - # # if commandType == 'scan': - # # retValue = self.wifiManager.GetWirelessNetworks() - # # if commandType == 'setup': - # # try: - # # ssid = parameters["ssid"] - # # password = parameters["password"] - # # interface = parameters["interface"] - # # retValue = self.wifiManager.Setup(ssid, password, interface) - # # except: - # # retValue = False - # # if commandService == 'services': - # # serviceName = parameters['ServiceName'] - # # if commandType == 'status': - # # retValue = self.serviceManager.Status(serviceName) - # # if commandType == 'start': - # # retValue = self.serviceManager.Start(serviceName) - # # if commandType == 'stop': - # # retValue = self.serviceManager.Stop(serviceName) - # if commandService == 'sensor': - # debug('SENSOR_COMMAND processing: ' + str(parameters)) - # method = None - # channel = None - # value = None - # driverClass = None - # sensorType = None - # sensorName = None - # if 'SensorName' in parameters: - # sensorName = parameters["SensorName"] - # if 'DriverClass' in parameters: - # driverClass = parameters["DriverClass"] - # if commandType == 'enable': - # sensor = None - # enable = None - # if 'Sensor' in parameters: - # sensor = parameters["Sensor"] - # if 'Enable' in parameters: - # enable = parameters["Enable"] - # retValue = self.sensorsClient.EnableSensor(sensor, enable) - # else: - # if commandType == 'edit': - # description = sensorName - # device = None - # if "Description" in parameters: - # description = parameters["Description"] - # if "Args" in parameters: - # args = parameters["Args"] - # retValue = self.sensorsClient.EditSensor(sensorName, description, driverClass, args) - # # else: - # # if 'Channel' in parameters: - # # channel = parameters["Channel"] - # # if 'Method' in parameters: - # # method = parameters["Method"] - # # if 'Value' in parameters: - # # value = parameters["Value"] - # # if 'SensorType' in parameters: - # # sensorType = parameters["SensorType"] - # # retValue = self.sensorsClient.SensorCommand(commandType, sensorName, sensorType, driverClass, method, channel, value) - # # if commandService == 'gpio': - # # method = parameters["Method"] - # # channel = parameters["Channel"] - # # value = parameters["Value"] - # # debug('ProcessDeviceCommand: ' + commandService + ' ' + method + ' ' + str(channel) + ' ' + str(value)) - # # retValue = str(self.sensorsClient.GpioCommand(commandType, method, channel, value)) - # # debug('ProcessDeviceCommand gpio returned value: ' + retValue) - # # if commandService == 'config': - # # try: - # # config_id = parameters["id"] - # # arguments = parameters["arguments"] - # # (retValue, output) = SystemConfig.ExecuteConfigCommand(config_id, arguments) - # # data["Output"] = output - # # retValue = str(retValue) - # # except: - # # exception("Exception on config") - # data['Response'] = retValue - # data['Id'] = id - # data['PacketType'] = PacketTypes.PT_DEVICE_COMMAND_RESPONSE.value - # data['MachineName'] = self.MachineId - # info('PT_DEVICE_COMMAND_RESPONSE: ' + dumps(data)) - # if sensorId: - # data['SensorId'] = sensorId - # self.EnqueuePacket(data) - def EnqueuePacket(self, message): """Enqueue a message packet to send to the server""" if isinstance(message, dict): @@ -947,14 +488,6 @@ def DequeuePacket(self): packet = None return packet - # def RequestSchedules(self): - # """Enqueue a packet to request schedules from the server""" - # data = {} - # data['MachineName'] = self.MachineId - # data['Stored'] = "dynamodb" - # data['PacketType'] = PacketTypes.PT_REQUEST_SCHEDULES.value - # self.EnqueuePacket(data) - def SendHistoryData(self): """Enqueue a packet containing historical data to send to the server""" try: diff --git a/myDevices/cloud/scheduler.py b/myDevices/cloud/scheduler.py index 5c96128..f29a944 100644 --- a/myDevices/cloud/scheduler.py +++ b/myDevices/cloud/scheduler.py @@ -193,7 +193,7 @@ def ProcessAction(self, scheduleItem): subject = scheduleItem.title #build an array of device names #if this fails to be sent, save it in the DB and resubmit it - runStatus = self.client.SendNotification(scheduleItem.notify, subject, body) + runStatus = False #self.client.SendNotification(scheduleItem.notify, subject, body) sleep(1) if runStatus == False: error('Notification ' + str(scheduleItem.notify) + ' was not sent') diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index b29b19d..3214d54 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -12,7 +12,6 @@ from myDevices.utils.daemon import Daemon from myDevices.cloud.dbmanager import DbManager from myDevices.utils.threadpool import ThreadPool -from hashlib import sha1 from myDevices.devices.bus import checkAllBus, BUSLIST from myDevices.devices.digital.gpio import NativeGPIO as GPIO from myDevices.devices import manager @@ -121,61 +120,6 @@ def SystemInformation(self): exception('SystemInformation failed') return newSystemInfo - # def SHA_Calc(self, object): - # """Return SHA value for an object""" - # if object == None: - # return '' - # try: - # strVal = dumps(object) - # except: - # exception('SHA_Calc failed for:' + str(object)) - # return '' - # return self.SHA_Calc_str(strVal) - - # def SHA_Calc_str(self, stringVal): - # """Return SHA value for a string""" - # m = sha1() - # m.update(stringVal.encode('utf8')) - # sDigest = str(m.hexdigest()) - # return sDigest - - # def AppendToDeviceList(self, device_list, source, device_type): - # """Append a sensor/actuator device to device list - - # Args: - # device_list: Device list to append device to - # source: Device to append to list - # device_type: Type of device - # """ - # device = source.copy() - # del device['origin'] - # device['type'] = device_type - # if len(source['type']) > 1: - # device['hash'] = self.SHA_Calc_str(device['name']+device['type']) - # else: - # device['hash'] = self.SHA_Calc_str(device['name']+device['device']) - # if device['hash'] in self.disabledSensors: - # device['enabled'] = 0 - # else: - # device['enabled'] = 1 - # device_list.append(device) - - # def GetDevices(self): - # """Return a list of current sensor/actuator devices""" - # manager.deviceDetector() - # device_list = manager.getDeviceList() - # devices = [] - # for dev in device_list: - # try: - # if len(dev['type']) == 0: - # self.AppendToDeviceList(devices, dev, '') - # else: - # for device_type in dev['type']: - # self.AppendToDeviceList(devices, dev, device_type) - # except: - # exception("Failed to get device: {}".format(dev)) - # return devices - def CallDeviceFunction(self, func, *args): """Call a function for a sensor/actuator device and format the result value type diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index 06998fc..ee8c64f 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -55,18 +55,6 @@ def getConfig(): config = {} if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): return config - # try: - # (returnCode, output) = SystemConfig.ExecuteConfigCommand(17, '') - # if output: - # values = output.strip().split(' ') - # configItem['Camera'] = {} - # for i in values: - # if '=' in i: - # val1 = i.split('=') - # configItem['Camera'][val1[0]] = int(val1[1]) - # del output - # except: - # exception('Get camera config') commands = {10: 'DeviceTree', 18: 'Serial', 20: 'OneWire', 21: 'I2C', 22: 'SPI'} for command, name in commands.items(): try: diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index b00a4c6..e60233d 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -67,29 +67,6 @@ def getMemoryInfo(self): exception('Error getting memory info') return memory_info - # def getUptime(self): - # """Get system uptime as a dict - - # Returned dict example:: - - # { - # 'uptime': 90844.69, - # 'idle': 391082.64 - # } - # """ - # info = {} - # uptime = 0.0 - # idle = 0.0 - # try: - # with open('/proc/uptime', 'r') as f_stat: - # lines = [line.split(' ') for content in f_stat.readlines() for line in content.split('\n') if line != ''] - # uptime = float(lines[0][0]) - # idle = float(lines[0][1]) - # except: - # exception('Error getting uptime') - # info['uptime'] = uptime - # return info - def getDiskInfo(self): """Get disk usage information as a list formatted for Cayenne MQTT From 9fdcf4c5670658d16da8c55fa321f1b7458122d7 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 19 Oct 2017 17:08:06 -0600 Subject: [PATCH 079/129] Clean up code, add logging. --- myDevices/cloud/client.py | 38 ++++++++--------------------------- myDevices/utils/subprocess.py | 2 +- myDevices/wifi/WirelessLib.py | 2 +- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index a33fecf..bd2ea8c 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -60,30 +60,9 @@ def __init__(self): continue key = splitLine[0].strip() value = splitLine[1].strip().replace('"', '') - if key == 'PRETTY_NAME': - self.PRETTY_NAME = value - continue - if key == 'NAME': - self.NAME = value - continue - if key == 'VERSION_ID': - self.VERSION_ID = value - continue - if key == 'VERSION': - self.VERSION = value - continue - if key == 'ID_LIKE': - self.ID_LIKE = value - continue - if key == 'ID': - self.ID = value - continue - if key == 'ANSI_COLOR': - self.ANSI_COLOR = value - continue - if key == 'HOME_URL': - self.HOME_URL = value - continue + keys = ('VERSION_ID', 'ID') + if key in keys: + setattr(self, key, value) except: exception("OSInfo Unexpected error") @@ -138,7 +117,7 @@ def run(self): if not message: info('WriterThread mqttClient no message, {}'.format(message)) continue - debug('WriterThread, topic: {} {}'.format(cayennemqtt.DATA_TOPIC, type(message))) + # debug('WriterThread, topic: {} {}'.format(cayennemqtt.DATA_TOPIC, message)) self.cloudClient.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, message) message = None except: @@ -277,12 +256,12 @@ def Destroy(self): def OnDataChanged(self, data): """Enqueue a packet containing changed system data to send to the server""" + info('Send changed data: {}'.format([{item['channel']:item['value']} for item in data])) self.EnqueuePacket(data) def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" try: - debug('SendSystemInfo') data = [] cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MAKE, value=self.hardware.getManufacturer()) cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MODEL, value=self.hardware.getModel()) @@ -297,6 +276,7 @@ def SendSystemInfo(self): cayennemqtt.DataChannel.add(data, channel, value=config[key]) except: pass + info('Send system info: {}'.format([{item['channel']:item['value']} for item in data])) self.EnqueuePacket(data) except Exception: exception('SendSystemInfo unexpected error') @@ -304,12 +284,12 @@ def SendSystemInfo(self): def SendSystemState(self): """Enqueue a packet containing system information to send to the server""" try: - debug('SendSystemState') data = [] download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) data += self.sensorsClient.systemData + info('Send system state: {} items'.format(len(data))) self.EnqueuePacket(data) except Exception as e: exception('ThreadSystemInfo unexpected error: ' + str(e)) @@ -473,9 +453,7 @@ def ProcessDeviceCommand(self, message): def EnqueuePacket(self, message): """Enqueue a message packet to send to the server""" - if isinstance(message, dict): - message['PacketTime'] = GetTime() - json_data = dumps(message) + '\n' + json_data = dumps(message) message = None self.writeQueue.put(json_data) diff --git a/myDevices/utils/subprocess.py b/myDevices/utils/subprocess.py index f26fe35..d5d501d 100644 --- a/myDevices/utils/subprocess.py +++ b/myDevices/utils/subprocess.py @@ -26,7 +26,7 @@ def executeCommand(command, increaseMemoryLimit=False): (stdout_data, stderr_data) = process.communicate() returncode = process.wait() returncode = process.returncode - debug('executeCommand: stdout_data {}, stderr_data {}'.format(stdout_data, stderr_data)) + # debug('executeCommand: stdout_data {}, stderr_data {}'.format(stdout_data, stderr_data)) if stdout_data: output = stdout_data.decode('utf-8') stdout_data = None diff --git a/myDevices/wifi/WirelessLib.py b/myDevices/wifi/WirelessLib.py index ed53447..1acb35c 100644 --- a/myDevices/wifi/WirelessLib.py +++ b/myDevices/wifi/WirelessLib.py @@ -1,5 +1,5 @@ from abc import ABCMeta, abstractmethod -import subprocess +# import subprocess from time import sleep from myDevices.utils.subprocess import executeCommand From a57c8aba9e8f6abccba5b42a329e3670f06cb8b3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 19 Oct 2017 17:25:08 -0600 Subject: [PATCH 080/129] Update CA cert path. Start download test after successful connection. --- myDevices/cloud/cayennemqtt.py | 3 +-- myDevices/cloud/client.py | 5 ++--- myDevices/cloud/download_speed.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 5fdc376..cbd5da2 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -99,8 +99,7 @@ def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', por self.client.on_message = self.message_callback self.client.username_pw_set(username, password) if port == 8883: - self.client.tls_set(ca_certs="/etc/mosquitto/certs/test-ca.crt", certfile="/etc/mosquitto/certs/cli2.crt", - keyfile="/etc/mosquitto/certs/cli2.key", tls_version=PROTOCOL_TLSv1_2) + self.client.tls_set(ca_certs='/etc/ssl/certs/ca-certificates.crt', tls_version=PROTOCOL_TLSv1_2) self.client.connect(hostname, port, 60) info("Connecting to %s..." % hostname) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index bd2ea8c..54d6ffa 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -211,14 +211,13 @@ def Initialize(self): self.PublicIP = ipgetter.myip() self.hardware = Hardware() self.oSInfo = OSInfo() - self.downloadSpeed = DownloadSpeed(self.config) - self.downloadSpeed.getDownloadSpeed() self.connected = False self.exiting = False self.Start() self.count = 10000 self.buff = bytearray(self.count) - #start thread only after init of other fields + self.downloadSpeed = DownloadSpeed(self.config) + self.downloadSpeed.getDownloadSpeed() self.sensorsClient.SetDataChanged(self.OnDataChanged) self.processManager = services.ProcessManager() self.serviceManager = services.ServiceManager() diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index fd0f828..b33a908 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -53,7 +53,7 @@ def TestDownload(self): try: self.interface = netifaces.gateways()['default'][netifaces.AF_INET][1] a = datetime.now() - info('Excuting regular download test for network speed') + info('Executing regular download test for network speed') url = self.config.cloudConfig.DownloadSpeedTestUrl if 'DownloadSpeedTestUrl' in self.config.cloudConfig else defaultUrl debug(url + ' ' + download_path) request.urlretrieve(url, download_path) From 66f45f84ffd07f8b6e6304787b77185a3c95f72b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 19 Oct 2017 17:48:08 -0600 Subject: [PATCH 081/129] Remove unneeded code. --- myDevices/cloud/client.py | 25 +++++-------------------- myDevices/sensors/sensors.py | 4 +--- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 54d6ffa..237dfd9 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -4,28 +4,23 @@ It also responds messages from the server, to set actuator values, change system config settings, etc. """ -from socket import SOCK_STREAM, socket, AF_INET, gethostname, SHUT_RDWR -from ssl import CERT_REQUIRED, wrap_socket from json import dumps, loads -from threading import Thread, RLock +from threading import Thread from time import strftime, localtime, tzset, time, sleep from queue import Queue, Empty from myDevices.utils.config import Config from myDevices.utils.logger import exception, info, warn, error, debug, logJson -from myDevices.system import services, ipgetter from myDevices.sensors import sensors from myDevices.system.hardware import Hardware -# from myDevices.wifi import WifiManager -from myDevices.cloud.scheduler import SchedulerEngine +# from myDevices.cloud.scheduler import SchedulerEngine from myDevices.cloud.download_speed import DownloadSpeed from myDevices.cloud.updater import Updater from myDevices.system.systemconfig import SystemConfig from myDevices.utils.daemon import Daemon from myDevices.utils.threadpool import ThreadPool -from myDevices.utils.history import History +# from myDevices.utils.history import History from myDevices.utils.subprocess import executeCommand -from select import select -from hashlib import sha256 +# from hashlib import sha256 from myDevices.cloud.apiclient import CayenneApiClient import myDevices.cloud.cayennemqtt as cayennemqtt @@ -161,10 +156,6 @@ def __init__(self, host, port, cayenneApiHost): self.HOST = host self.PORT = port self.CayenneApiHost = cayenneApiHost - self.onMessageReceived = None - self.onMessageSent = None - self.initialized = False - self.machineName = gethostname() self.config = Config(APP_SETTINGS) inviteCode = self.config.get('Agent', 'InviteCode', fallback=None) if not inviteCode: @@ -192,11 +183,10 @@ def __init__(self, host, port, cayenneApiHost): self.password = None self.clientId = None self.CheckSubscription() - self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') + # self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.Initialize() self.updater = Updater(self.config) self.updater.start() - self.initialized = True def __del__(self): """Delete the client""" @@ -205,10 +195,8 @@ def __del__(self): def Initialize(self): """Initialize server connection and background threads""" try: - self.mutex = RLock() self.readQueue = Queue() self.writeQueue = Queue() - self.PublicIP = ipgetter.myip() self.hardware = Hardware() self.oSInfo = OSInfo() self.connected = False @@ -219,15 +207,12 @@ def Initialize(self): self.downloadSpeed = DownloadSpeed(self.config) self.downloadSpeed.getDownloadSpeed() self.sensorsClient.SetDataChanged(self.OnDataChanged) - self.processManager = services.ProcessManager() - self.serviceManager = services.ServiceManager() self.writerThread = WriterThread('writer', self) self.writerThread.start() self.processorThread = ProcessorThread('processor', self) self.processorThread.start() TimerThread(self.SendSystemInfo, 300) TimerThread(self.SendSystemState, 30, 5) - self.previousSystemInfo = None # self.sentHistoryData = {} # self.historySendFails = 0 # self.historyThread = Thread(target=self.SendHistoryData) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 3214d54..6a708ce 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -5,7 +5,7 @@ from myDevices.utils.logger import exception, info, warn, error, debug, logJson from time import sleep, time from json import loads, dumps -from threading import Thread, RLock +from threading import RLock from myDevices.system import services from datetime import datetime, timedelta from os import path, getpid @@ -35,9 +35,7 @@ def __init__(self): self.systemData = [] self.currentSystemState = [] self.disabledSensors = {} - self.retrievingSystemInfo = False self.disabledSensorTable = "disabled_sensors" - self.systemInfoRefreshList = [] checkAllBus() self.gpio = GPIO() manager.addDeviceInstance("GPIO", "GPIO", "GPIO", self.gpio, [], "system") From a6f15a272ea0a0e42f44e78d6a200f9ab9db66f3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 19 Oct 2017 18:01:33 -0600 Subject: [PATCH 082/129] Update debug function name. --- myDevices/sensors/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 6a708ce..58cb80a 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -335,7 +335,7 @@ def GpioCommand(self, command, channel, value): return str(self.gpio.setFunctionString(channel, 'out')) elif command in ('value', ''): return self.gpio.digitalWrite(channel, value) - debug.log('GpioCommand not set') + debug('GpioCommand not set') return 'failure' def SensorCommand(self, command, sensorId, channel, value): From a92c1d1ab97efcb5519b51af88e5bc604b60d4ce Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 23 Oct 2017 12:46:49 -0600 Subject: [PATCH 083/129] Use MQTT v1 topics. --- myDevices/cloud/cayennemqtt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index cbd5da2..acfc45d 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -5,9 +5,9 @@ from myDevices.utils.logger import debug, error, exception, info, logJson, warn # Topics -DATA_TOPIC = 'data.json' +DATA_TOPIC = 'data/json' COMMAND_TOPIC = 'cmd' -COMMAND_RESPONSE_TOPIC = 'cmd.res' +COMMAND_RESPONSE_TOPIC = 'response' # Data Channels SYS_HARDWARE_MAKE = 'sys:hw:make' @@ -88,11 +88,11 @@ def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', por username is the Cayenne username. password is the Cayenne password. - clientID is the Cayennne client ID for the device. + clientid is the Cayennne client ID for the device. hostname is the MQTT broker hostname. port is the MQTT broker port. """ - self.root_topic = "v2/things/%s" % clientid + self.root_topic = 'v1/{}/things/{}'.format(username, clientid) self.client = mqtt.Client(client_id=clientid, clean_session=True, userdata=self) self.client.on_connect = self.connect_callback self.client.on_disconnect = self.disconnect_callback From e014c4b17fd107f25ce5344454e46ea7f0b845b6 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 23 Oct 2017 12:47:14 -0600 Subject: [PATCH 084/129] Update analog sensor test. --- myDevices/test/sensors_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/myDevices/test/sensors_test.py b/myDevices/test/sensors_test.py index 3258a55..50ab93c 100644 --- a/myDevices/test/sensors_test.py +++ b/myDevices/test/sensors_test.py @@ -108,7 +108,8 @@ def testSensorInfo(self): #Test getting analog value channel = 'dev:{}'.format(sensors['distance']['name']) retrievedSensorInfo = next(obj for obj in SensorsClientTest.client.SensorsInfo() if obj['channel'] == channel) - self.assertEqual(retrievedSensorInfo['value'], 0.0) + self.assertGreaterEqual(retrievedSensorInfo['value'], 0.0) + self.assertLessEqual(retrievedSensorInfo['value'], 1.0) for sensor in sensors.values(): self.assertTrue(SensorsClientTest.client.RemoveSensor(sensor['name'])) From 8cccc464666e715129cd68b5d679e9cc15dd79c8 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 23 Oct 2017 16:59:59 -0600 Subject: [PATCH 085/129] Use secure MQTT port by default. --- myDevices/__main__.py | 2 +- myDevices/cloud/cayennemqtt.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/myDevices/__main__.py b/myDevices/__main__.py index a5f0e15..642eb6e 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -123,7 +123,7 @@ def main(argv): logToFile(logfile) config = Config(configfile) HOST = config.get('CONFIG', 'ServerAddress', 'mqtt.mydevices.com') - PORT = config.getInt('CONFIG', 'ServerPort', 1883) + PORT = config.getInt('CONFIG', 'ServerPort', 8883) CayenneApiHost = config.get('CONFIG', 'CayenneApi', 'https://api.mydevices.com') global client client = CloudServerClient(HOST, PORT, CayenneApiHost) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index acfc45d..91311dc 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -83,7 +83,7 @@ class CayenneMQTTClient: connected = False on_message = None - def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', port=1883): + def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', port=8883): """Initializes the client and connects to Cayenne. username is the Cayenne username. @@ -98,10 +98,10 @@ def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', por self.client.on_disconnect = self.disconnect_callback self.client.on_message = self.message_callback self.client.username_pw_set(username, password) - if port == 8883: + if port != 1883: self.client.tls_set(ca_certs='/etc/ssl/certs/ca-certificates.crt', tls_version=PROTOCOL_TLSv1_2) self.client.connect(hostname, port, 60) - info("Connecting to %s..." % hostname) + info('Connecting to {}:{}'.format(hostname, port)) def connect_callback(self, client, userdata, flags, rc): """The callback for when the client connects to the server. From 255fc347488d23fbccef85e69384bac3b2b264ec Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 13 Nov 2017 15:48:31 -0700 Subject: [PATCH 086/129] Get MQTT credentials from server. --- myDevices/cloud/apiclient.py | 8 +-- myDevices/cloud/client.py | 100 ++++++++++++++++------------------- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index 82393ab..84ad048 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -47,20 +47,20 @@ def activate(self, inviteCode): url = '/things/key/activate' return self.sendRequest('POST', url, body) - def getId(self, content): + def getCredentials(self, content): if content is None: return None body = content.decode("utf-8") if body is None or body is "": return None - return json.loads(body)['id'] + return json.loads(body) def loginDevice(self, inviteCode): response = self.authenticate(inviteCode) if response and response.status_code == 200: - return self.getId(response.content) + return self.getCredentials(response.content) if not response or response.status_code == 412: response = self.activate(inviteCode) if response and response.status_code == 200: - return self.getId(response.content) + return self.getCredentials(response.content) return None diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 237dfd9..aee9445 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -178,7 +178,6 @@ def __init__(self, host, port, cayenneApiHost): self.config.set('Agent', 'InstallDate', self.installDate) self.networkConfig = Config(NETWORK_SETTINGS) self.sensorsClient = sensors.SensorsClient() - self.MachineId = None self.username = None self.password = None self.clientId = None @@ -290,15 +289,12 @@ def CheckSubscription(self): info('Registration succeeded for invite code {}, credentials = {}'.format(inviteCode, credentials)) self.config.set('Agent', 'Initialized', 'true') try: - self.MachineId = credentials #credentials['id'] - self.username = 'username' #credentials['mqtt']['username'] - self.password = 'password' #credentials['mqtt']['password'] - self.clientId = 'client_id' #credentials['mqtt']['clientId'] - self.config.set('Agent', 'Id', self.MachineId) + self.username = credentials['mqtt']['username'] + self.password = credentials['mqtt']['password'] + self.clientId = credentials['mqtt']['clientId'] except: exception('Invalid credentials, closing the process') Daemon.Exit() - info('CheckSubscription: MachineId {}'.format(self.MachineId)) def Start(self): """Connect to the server""" @@ -351,12 +347,6 @@ def OnMessage(self, message): def RunAction(self, action): """Run a specified action""" debug('RunAction') - if 'MachineName' in action: - #Use the config file machine if self.MachineId has not been set yet due to connection issues - machine_id = self.MachineId if self.MachineId else self.config.get('Agent', 'Id') - if machine_id != action['MachineName']: - debug('Scheduler action is not assigned for this machine: ' + str(action)) - return self.ExecuteMessage(action) def ProcessMessage(self): @@ -450,45 +440,45 @@ def DequeuePacket(self): packet = None return packet - def SendHistoryData(self): - """Enqueue a packet containing historical data to send to the server""" - try: - info('SendHistoryData start') - history = History() - history.Reset() - while True: - try: - #If there is no acknowledgment after a minute we assume failure - sendFailed = [key for key, item in self.sentHistoryData.items() if (item['Timestamp'] + 60) < time()] - info('SendHistoryData previously SendFailed items: ' + str(sendFailed)) - for id in sendFailed: - self.historySendFails += len(sendFailed) - history.Sent(False, self.sentHistoryData[id]['HistoryData']) - del self.sentHistoryData[id] - historyData = history.GetHistoricalData() - if historyData: - data = {} - info('SendHistoryData historyData: ' + str(historyData)) - data['MachineName'] = self.MachineId - data['Timestamp'] = int(time()) - data['PacketType'] = PacketTypes.PT_HISTORY_DATA.value - id = sha256(dumps(historyData).encode('utf8')).hexdigest() - data['Id'] = id - data['HistoryData'] = historyData - info('Sending history data, id = {}'.format(id)) - debug('SendHistoryData historyData: ' + str(data)) - self.EnqueuePacket(data) - #this will keep accumulating - self.sentHistoryData[id] = data - except Exception as ex: - exception('SendHistoryData error' + str(ex)) - delay = 60 - if self.historySendFails > 2: - delay = 120 - if self.historySendFails > 4: - #Wait an hour if we keep getting send failures. - delay = 3600 - self.historySendFails = 0 - sleep(delay) - except Exception as ex: - exception('SendHistoryData general exception: ' + str(ex)) + # def SendHistoryData(self): + # """Enqueue a packet containing historical data to send to the server""" + # try: + # info('SendHistoryData start') + # history = History() + # history.Reset() + # while True: + # try: + # #If there is no acknowledgment after a minute we assume failure + # sendFailed = [key for key, item in self.sentHistoryData.items() if (item['Timestamp'] + 60) < time()] + # info('SendHistoryData previously SendFailed items: ' + str(sendFailed)) + # for id in sendFailed: + # self.historySendFails += len(sendFailed) + # history.Sent(False, self.sentHistoryData[id]['HistoryData']) + # del self.sentHistoryData[id] + # historyData = history.GetHistoricalData() + # if historyData: + # data = {} + # info('SendHistoryData historyData: ' + str(historyData)) + # data['MachineName'] = self.MachineId + # data['Timestamp'] = int(time()) + # data['PacketType'] = PacketTypes.PT_HISTORY_DATA.value + # id = sha256(dumps(historyData).encode('utf8')).hexdigest() + # data['Id'] = id + # data['HistoryData'] = historyData + # info('Sending history data, id = {}'.format(id)) + # debug('SendHistoryData historyData: ' + str(data)) + # self.EnqueuePacket(data) + # #this will keep accumulating + # self.sentHistoryData[id] = data + # except Exception as ex: + # exception('SendHistoryData error' + str(ex)) + # delay = 60 + # if self.historySendFails > 2: + # delay = 120 + # if self.historySendFails > 4: + # #Wait an hour if we keep getting send failures. + # delay = 3600 + # self.historySendFails = 0 + # sleep(delay) + # except Exception as ex: + # exception('SendHistoryData general exception: ' + str(ex)) From 6863bdda4e14c2f7817015df4fbe63e05d29d41f Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 16 Nov 2017 18:44:14 -0700 Subject: [PATCH 087/129] Set config values using new data channel. --- myDevices/cloud/cayennemqtt.py | 2 +- myDevices/cloud/client.py | 22 +++++++++++++++++----- myDevices/cloud/download_speed.py | 6 +++--- myDevices/cloud/updater.py | 31 ++++++++++--------------------- myDevices/utils/config.py | 11 ++++++----- 5 files changed, 37 insertions(+), 35 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 91311dc..f7cd89c 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -26,7 +26,7 @@ SYS_POWER = 'sys:pwr' AGENT_VERSION = 'agent:version' AGENT_DEVICES = 'agent:devices' -AGENT_UNINSTALL = 'agent:uninstall' +AGENT_MANAGE = 'agent:manage' DEV_SENSOR = 'dev' # Channel Suffixes diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index aee9445..f89d0e6 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -375,10 +375,8 @@ def ExecuteMessage(self, message): self.ProcessDeviceCommand(message) elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): self.ProcessConfigCommand(message) - elif channel == cayennemqtt.AGENT_UNINSTALL: - executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh') - # elif channel == cayennemqtt.AGENT_CONFIG: - # self.config.setCloudConfig(message['payload']) + elif channel == cayennemqtt.AGENT_MANAGE: + self.ProcessAgentCommand(message) else: info('Unknown message') @@ -388,6 +386,20 @@ def ProcessPowerCommand(self, message): output, result = executeCommand(commands[message['payload']]) debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output)) + def ProcessAgentCommand(self, message): + """Process command to manage the agent state""" + if message['suffix'] == 'uninstall': + output, result = executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh') + debug('ProcessAgentCommand: {}, result: {}, output: {}'.format(message, result, output)) + elif message['suffix'] == 'config': + for key, value in message['payload'].items(): + if value is None: + info('Remove config item: {}'.format(key)) + self.config.remove('Agent', key) + else: + info('Set config item: {} {}'.format(key, value)) + self.config.set('Agent', key, value) + def ProcessConfigCommand(self, message): """Process system configuration command""" value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable @@ -481,4 +493,4 @@ def DequeuePacket(self): # self.historySendFails = 0 # sleep(delay) # except Exception as ex: - # exception('SendHistoryData general exception: ' + str(ex)) + # exception('SendHistoryData general exception: ' + str(ex)) \ No newline at end of file diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index b33a908..c775874 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -22,7 +22,7 @@ class DownloadSpeed(): """Class for checking download speed""" def __init__(self, config): - """Initialize variable and start download speed test""" + """Initialize variables and start download speed test""" self.downloadSpeed = None self.testTime = None self.isRunning = False @@ -54,7 +54,7 @@ def TestDownload(self): self.interface = netifaces.gateways()['default'][netifaces.AF_INET][1] a = datetime.now() info('Executing regular download test for network speed') - url = self.config.cloudConfig.DownloadSpeedTestUrl if 'DownloadSpeedTestUrl' in self.config.cloudConfig else defaultUrl + url = self.config.get('Agent', 'DownloadSpeedTestUrl', defaultUrl) debug(url + ' ' + download_path) request.urlretrieve(url, download_path) request.urlcleanup() @@ -78,7 +78,7 @@ def IsDownloadTime(self): """Return true if it is time to run a new download speed test""" if self.testTime is None: return True - downloadRate = int(self.config.cloudConfig.DownloadSpeedTestRate) if 'DownloadSpeedTestRate' in self.config.cloudConfig else defaultDownloadRate + downloadRate = self.config.getInt('Agent', 'DownloadSpeedTestRate', defaultDownloadRate) if self.testTime + timedelta(seconds=downloadRate+self.delay ) < datetime.now(): return True return False diff --git a/myDevices/cloud/updater.py b/myDevices/cloud/updater.py index 742dd8f..7749d4d 100644 --- a/myDevices/cloud/updater.py +++ b/myDevices/cloud/updater.py @@ -41,21 +41,13 @@ def __init__(self, config, onUpdateConfig = None): self.env = self.appSettings.get('Agent','Environment', fallback='live') global SETUP_URL global UPDATE_URL - global TIME_TO_CHECK - - if self.env == "live": + if self.env == 'live': SETUP_URL = SETUP_URL + SETUP_NAME else: - SETUP_URL = SETUP_URL + self.env + "_" + SETUP_NAME + SETUP_URL = SETUP_URL + self.env + '_' + SETUP_NAME UPDATE_URL = UPDATE_URL + self.env - - if 'UpdateUrl' in config.cloudConfig: - UPDATE_URL = config.cloudConfig.UpdateUrl - if 'UpdateCheckRate' in config.cloudConfig: - interval = int(config.cloudConfig.UpdateCheckRate) - TIME_TO_CHECK = interval + random.randint(0, interval*10) - if 'SetupUrl' in config.cloudConfig: - SETUP_URL = config.cloudConfig.SetupUrl + UPDATE_URL = self.appSettings.get('Agent', 'UpdateUrl', UPDATE_URL) + SETUP_URL = self.appSettings.get('Agent', 'SetupUrl', SETUP_URL) self.scheduler = scheduler(time, sleep) self.Continue = True self.currentVersion = '' @@ -98,14 +90,13 @@ def CheckUpdate(self): elapsedTime=now-self.startTime if elapsedTime.total_seconds() < TIME_TO_CHECK: return - self.startTime = datetime.now() if path.exists(UPDATE_PATH) == True: error('myDevices updater another update in progress') return sleep(1) # Run the update as root - executeCommand("sudo python3 -m myDevices.cloud.doupdatecheck") + executeCommand('sudo python3 -m myDevices.cloud.doupdatecheck') def DoUpdateCheck(self): mkdir(UPDATE_PATH) @@ -114,13 +105,11 @@ def DoUpdateCheck(self): self.currentVersion = self.appSettings.get('Agent', 'Version', fallback='1.0.1.0') except: error('Updater Current Version not found') - sleep(1) if not self.currentVersion: error('Current version not available. Cannot update agent.') self.UpdateCleanup() return - retValue = self.RetrieveUpdate() sleep(1) if retValue is False: @@ -129,7 +118,6 @@ def DoUpdateCheck(self): return sleep(1) retValue = self.CheckVersion(self.currentVersion, self.newVersion) - if retValue is True: info('Update needed, current version: {}, update version: {}'.format(self.currentVersion, self.newVersion)) retValue = self.ExecuteUpdate() @@ -143,15 +131,17 @@ def DoUpdateCheck(self): self.UpdateCleanup() def SetupUpdater(self): + global TIME_TO_CHECK + TIME_TO_CHECK = self.appSettings.get('Agent', 'UpdateCheckRate', TIME_TO_CHECK) self.scheduler.enter(TIME_TO_CHECK, 1, self.CheckUpdate, ()) def RetrieveUpdate(self): try: info('Checking update version') - debug( UPDATE_URL + ' ' + UPDATE_CFG ) + debug('Retrieve update config: {} {}'.format(UPDATE_URL, UPDATE_CFG)) retValue = self.DownloadFile(UPDATE_URL, UPDATE_CFG) if retValue is False: - error('failed to download update file') + error('Failed to download update file') return retValue updateConfig = Config(UPDATE_CFG) try: @@ -159,7 +149,6 @@ def RetrieveUpdate(self): self.downloadUrl = updateConfig.get('UPDATES','Url') except: error('Updater missing: update version or Url') - info('Updater retrieve update success') return True except: @@ -179,7 +168,7 @@ def DownloadFile(self, url, localPath): return False def ExecuteUpdate(self): - debug('') + debug('Execute update: {} {}'.format(SETUP_URL, SETUP_PATH)) retValue = self.DownloadFile(SETUP_URL, SETUP_PATH) if retValue is False: return retValue diff --git a/myDevices/utils/config.py b/myDevices/utils/config.py index 9d506db..cd49baf 100644 --- a/myDevices/utils/config.py +++ b/myDevices/utils/config.py @@ -7,7 +7,6 @@ def __init__(self, path): self.path = path self.config = RawConfigParser() self.config.optionxform = str - self.cloudConfig = {} try: with open(path) as fp: self.config.readfp(fp) @@ -21,13 +20,18 @@ def set(self, section, key, value): except NoSectionError: self.config.add_section(section) self.config.set(section, key, value) - return self.save() + self.save() def get(self, section, key, fallback=_UNSET): return self.config.get(section, key, fallback=fallback) def getInt(self, section, key, fallback=_UNSET): return self.config.getint(section, key, fallback=fallback) + + def remove(self, section, key): + with self.mutex: + result = self.config.remove_option(section, key) + self.save() def save(self): with self.mutex: @@ -37,7 +41,4 @@ def save(self): def sections(self): return self.config.sections() - def setCloudConfig(self, cloudConfig): - self.cloudConfig = cloudConfig - From 7c0ee3477748e04093c050a71160773bdc112b03 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 17 Nov 2017 12:20:31 -0700 Subject: [PATCH 088/129] Fix errors when installing on a clean system. --- myDevices/__init__.py | 5 ++++- myDevices/cloud/client.py | 3 ++- setup.py | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/myDevices/__init__.py b/myDevices/__init__.py index 8b13789..b60b18d 100644 --- a/myDevices/__init__.py +++ b/myDevices/__init__.py @@ -1 +1,4 @@ - +""" +This package contains the Cayenne agent, which is a full featured client for the Cayenne IoT project builder: https://cayenne.mydevices.com. +""" +__version__ = '0.2.1' diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index f89d0e6..1f07e80 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -8,6 +8,7 @@ from threading import Thread from time import strftime, localtime, tzset, time, sleep from queue import Queue, Empty +from myDevices import __version__ from myDevices.utils.config import Config from myDevices.utils.logger import exception, info, warn, error, debug, logJson from myDevices.sensors import sensors @@ -250,7 +251,7 @@ def SendSystemInfo(self): cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MODEL, value=self.hardware.getModel()) cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) - cayennemqtt.DataChannel.add(data, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent','Version')) + cayennemqtt.DataChannel.add(data, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent', 'Version', __version__)) config = SystemConfig.getConfig() if config: channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} diff --git a/setup.py b/setup.py index cf4a146..51bd44a 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ import os import pwd import grp +from myDevices import __version__ from myDevices.system.hardware import Hardware @@ -25,7 +26,7 @@ user_id = int(os.environ['SUDO_UID']) group_id = int(os.environ['SUDO_GID']) username = pwd.getpwuid(user_id).pw_name -directories = ('/etc/myDevices', '/var/log/myDevices', '/var/run/myDevices') +directories = ('/etc/myDevices', '/etc/myDevices/scripts', '/var/log/myDevices', '/var/run/myDevices') for directory in directories: try: os.makedirs(directory) @@ -34,7 +35,7 @@ os.chown(directory, user_id, group_id) setup(name = 'myDevices', - version = '0.2.1', + version = __version__, author = 'myDevices', author_email = 'N/A', description = 'myDevices Cayenne agent', From 7238746f0289ad12ca4ae27d580a5a3a28e5785f Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 17 Nov 2017 18:23:48 -0700 Subject: [PATCH 089/129] Send response after processing commands. --- myDevices/cloud/cayennemqtt.py | 10 ++- myDevices/cloud/client.py | 146 +++++++++++++++++++++---------- myDevices/sensors/sensors.py | 2 +- myDevices/system/systemconfig.py | 4 +- 4 files changed, 110 insertions(+), 52 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index f7cd89c..3480ca6 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -7,7 +7,7 @@ # Topics DATA_TOPIC = 'data/json' COMMAND_TOPIC = 'cmd' -COMMAND_RESPONSE_TOPIC = 'response' +COMMAND_RESPONSE_TOPIC = 'cmd.res' # Data Channels SYS_HARDWARE_MAKE = 'sys:hw:make' @@ -157,8 +157,14 @@ def message_callback(self, client, userdata, msg): message = {} try: message['payload'] = loads(msg.payload.decode()) + message['msg_id'] = message['payload']['msg_id'] except decoder.JSONDecodeError: - message['payload'] = msg.payload.decode() + payload = msg.payload.decode().split(',') + if len(payload) > 1: + message['msg_id'] = payload[0] + message['payload'] = payload[1] + else: + message['payload'] = payload[0] channel = msg.topic.split('/')[-1].split(';') message['channel'] = channel[0] if len(channel) > 1: diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 1f07e80..cb8af5c 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -109,12 +109,14 @@ def run(self): if self.cloudClient.mqttClient.connected == False: info('WriterThread mqttClient not connected') continue - message = self.cloudClient.DequeuePacket() + topic, message = self.cloudClient.DequeuePacket() if not message: info('WriterThread mqttClient no message, {}'.format(message)) continue - # debug('WriterThread, topic: {} {}'.format(cayennemqtt.DATA_TOPIC, message)) - self.cloudClient.mqttClient.publish_packet(cayennemqtt.DATA_TOPIC, message) + # debug('WriterThread, topic: {} {}'.format(topic, message)) + if not isinstance(message, str): + message = dumps(message) + self.cloudClient.mqttClient.publish_packet(topic, message) message = None except: exception("WriterThread Unexpected error") @@ -383,74 +385,124 @@ def ExecuteMessage(self, message): def ProcessPowerCommand(self, message): """Process command to reboot/shutdown the system""" - commands = {'reset': 'sudo shutdown -r now', 'halt': 'sudo shutdown -h now'} - output, result = executeCommand(commands[message['payload']]) - debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output)) + error = None + try: + commands = {'reset': 'sudo shutdown -r now', 'halt': 'sudo shutdown -h now'} + output, result = executeCommand(commands[message['payload']]) + debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output)) + if result != 0: + error = 'Error executing shutdown command' + except Exception as ex: + error = '{}: {}'.format(type(ex).__name__, ex) + self.EnqueueCommandResponse(message, error) def ProcessAgentCommand(self, message): """Process command to manage the agent state""" - if message['suffix'] == 'uninstall': - output, result = executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh') - debug('ProcessAgentCommand: {}, result: {}, output: {}'.format(message, result, output)) - elif message['suffix'] == 'config': - for key, value in message['payload'].items(): - if value is None: - info('Remove config item: {}'.format(key)) - self.config.remove('Agent', key) - else: - info('Set config item: {} {}'.format(key, value)) - self.config.set('Agent', key, value) + error = None + try: + if message['suffix'] == 'uninstall': + output, result = executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh') + debug('ProcessAgentCommand: {}, result: {}, output: {}'.format(message, result, output)) + if result != 0: + error = 'Error uninstalling agent' + elif message['suffix'] == 'config': + for key, value in message['payload'].items(): + if value is None: + info('Remove config item: {}'.format(key)) + self.config.remove('Agent', key) + else: + info('Set config item: {} {}'.format(key, value)) + self.config.set('Agent', key, value) + except Exception as ex: + error = '{}: {}'.format(type(ex).__name__, ex) + self.EnqueueCommandResponse(message, error) def ProcessConfigCommand(self, message): """Process system configuration command""" - value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable - command_id = {cayennemqtt.SYS_I2C: 11, cayennemqtt.SYS_SPI: 12, cayennemqtt.SYS_UART: 13, cayennemqtt.SYS_DEVICETREE: 9} - result, output = SystemConfig.ExecuteConfigCommand(command_id[message['channel']], value) - debug('ProcessConfigCommand: {}, result: {}, output: {}'.format(message, result, output)) + error = None + try: + value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable + command_id = {cayennemqtt.SYS_I2C: 11, cayennemqtt.SYS_SPI: 12, cayennemqtt.SYS_UART: 13, cayennemqtt.SYS_DEVICETREE: 9} + result, output = SystemConfig.ExecuteConfigCommand(command_id[message['channel']], value) + debug('ProcessConfigCommand: {}, result: {}, output: {}'.format(message, result, output)) + if result != 0: + error = 'Error executing config command' + except Exception as ex: + error = '{}: {}'.format(type(ex).__name__, ex) + self.EnqueueCommandResponse(message, error) def ProcessGpioCommand(self, message): """Process GPIO command""" - channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', '')) - result = self.sensorsClient.GpioCommand(message['suffix'], channel, message['payload']) - debug('ProcessGpioCommand result: {}'.format(result)) + error = None + try: + channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', '')) + result = self.sensorsClient.GpioCommand(message['suffix'], channel, message['payload']) + debug('ProcessGpioCommand result: {}'.format(result)) + if result == 'failure': + error = 'GPIO command failed' + except Exception as ex: + error = '{}: {}'.format(type(ex).__name__, ex) + self.EnqueueCommandResponse(message, error) def ProcessSensorCommand(self, message): """Process sensor command""" - sensor_info = message['channel'].replace(cayennemqtt.DEV_SENSOR + ':', '').split(':') - sensor = sensor_info[0] - channel = None - if len(sensor_info) > 1: - channel = sensor_info[1] - result = self.sensorsClient.SensorCommand(message['suffix'], sensor, channel, message['payload']) - debug('ProcessSensorCommand result: {}'.format(result)) + error = None + try: + sensor_info = message['channel'].replace(cayennemqtt.DEV_SENSOR + ':', '').split(':') + sensor = sensor_info[0] + channel = None + if len(sensor_info) > 1: + channel = sensor_info[1] + result = self.sensorsClient.SensorCommand(message['suffix'], sensor, channel, message['payload']) + debug('ProcessSensorCommand result: {}'.format(result)) + if result is False: + error = 'Sensor command failed' + except Exception as ex: + error = '{}: {}'.format(type(ex).__name__, ex) + self.EnqueueCommandResponse(message, error) def ProcessDeviceCommand(self, message): """Process a device command to add/edit/remove a sensor""" - payload = message['payload'] - info('ProcessDeviceCommand payload: {}'.format(payload)) - if message['suffix'] == 'add': - result = self.sensorsClient.AddSensor(payload['id'], payload['description'], payload['class'], payload['args']) - elif message['suffix'] == 'edit': - result = self.sensorsClient.EditSensor(payload['id'], payload['description'], payload['class'], payload['args']) - elif message['suffix'] == 'delete': - result = self.sensorsClient.RemoveSensor(payload['id']) + error = None + try: + payload = message['payload'] + info('ProcessDeviceCommand payload: {}'.format(payload)) + if message['suffix'] == 'add': + result = self.sensorsClient.AddSensor(payload['id'], payload['description'], payload['class'], payload['args']) + elif message['suffix'] == 'edit': + result = self.sensorsClient.EditSensor(payload['id'], payload['description'], payload['class'], payload['args']) + elif message['suffix'] == 'delete': + result = self.sensorsClient.RemoveSensor(payload['id']) + else: + info('Unknown device command: {}'.format(message['suffix'])) + debug('ProcessDeviceCommand result: {}'.format(result)) + if result is False: + error = 'Device command failed' + except Exception as ex: + error = '{}: {}'.format(type(ex).__name__, ex) + self.EnqueueCommandResponse(message, error) + + def EnqueueCommandResponse(self, message, error): + """Send response after processing a command message""" + debug('EnqueueCommandResponse error: {}'.format(error)) + if error: + response = '{},error={}'.format(message['msg_id'], error) else: - info('Unknown device command: {}'.format(message['suffix'])) - debug('ProcessDeviceCommand result: {}'.format(result)) + response = '{},ok'.format(message['msg_id']) + self.EnqueuePacket(response, cayennemqtt.COMMAND_RESPONSE_TOPIC) - def EnqueuePacket(self, message): + def EnqueuePacket(self, message, topic=cayennemqtt.DATA_TOPIC): """Enqueue a message packet to send to the server""" - json_data = dumps(message) - message = None - self.writeQueue.put(json_data) + packet = (topic, message) + self.writeQueue.put(packet) def DequeuePacket(self): """Dequeue a message packet to send to the server""" - packet = None + packet = (None, None) try: packet = self.writeQueue.get() except Empty: - packet = None + pass return packet # def SendHistoryData(self): diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 58cb80a..80aa74a 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -335,7 +335,7 @@ def GpioCommand(self, command, channel, value): return str(self.gpio.setFunctionString(channel, 'out')) elif command in ('value', ''): return self.gpio.digitalWrite(channel, value) - debug('GpioCommand not set') + debug('GPIO command failed') return 'failure' def SensorCommand(self, command, sensorId, channel, value): diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index ee8c64f..4ee690d 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -39,11 +39,11 @@ def ExecuteConfigCommand(config_id, parameters=''): (output, returnCode) = executeCommand(command) debug('ExecuteConfigCommand '+ str(config_id) + ' args: ' + str(parameters) + ' retCode: ' + str(returnCode) + ' output: ' + output ) if "reboot required" in output: - ThreadPool.Submit(SystemConfig.RestartService) + ThreadPool.Submit(SystemConfig.RestartDevice) return (returnCode, output) @staticmethod - def RestartService(): + def RestartDevice(): """Reboot the device""" sleep(5) command = "sudo shutdown -r now" From 5b10b83b486bc9f0dd81929e709e234a323c0f5e Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 21 Nov 2017 12:40:24 -0700 Subject: [PATCH 090/129] Update version number and package info. --- myDevices/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/myDevices/__init__.py b/myDevices/__init__.py index b60b18d..c6a0fa5 100644 --- a/myDevices/__init__.py +++ b/myDevices/__init__.py @@ -1,4 +1,4 @@ """ -This package contains the Cayenne agent, which is a full featured client for the Cayenne IoT project builder: https://cayenne.mydevices.com. +This package contains the Cayenne agent, which is a full featured client for the Cayenne IoT project builder: https://cayenne.mydevices.com. It sends system information as well as sensor and actuator data and responds to actuator messages initiated from the Cayenne dashboard and mobile apps. """ -__version__ = '0.2.1' +__version__ = '1.1.0' From 4629da1b2e4ae754eeb6e16228d42692eee23627 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 21 Nov 2017 15:49:03 -0700 Subject: [PATCH 091/129] Install paho-mqtt dependency. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 51bd44a..215541a 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ url = 'https://www.mydevices.com/', classifiers = classifiers, packages = ["myDevices", "myDevices.cloud", "myDevices.utils", "myDevices.system", "myDevices.sensors" , "myDevices.wifi", "myDevices.schedule", "myDevices.requests_futures", "myDevices.devices", "myDevices.devices.analog", "myDevices.devices.digital", "myDevices.devices.sensor", "myDevices.devices.shield", "myDevices.decorators"], - install_requires = ['enum34', 'iwlib', 'jsonpickle', 'netifaces >= 0.10.5', 'psutil >= 0.7.0', 'requests'], + install_requires = ['enum34', 'iwlib', 'jsonpickle', 'netifaces >= 0.10.5', 'psutil >= 0.7.0', 'requests', 'paho-mqtt'], data_files = [('/etc/myDevices/scripts', ['scripts/config.sh'])] ) From befd596d7b948c913b7b47af31c52da44e09065c Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 21 Nov 2017 18:44:51 -0700 Subject: [PATCH 092/129] Fix issues preventing and slowing process exit. --- myDevices/__main__.py | 10 ++- myDevices/cloud/client.py | 114 +++++++++++++++--------------- myDevices/cloud/download_speed.py | 8 +-- myDevices/sensors/sensors.py | 23 +++--- myDevices/utils/daemon.py | 6 +- 5 files changed, 81 insertions(+), 80 deletions(-) diff --git a/myDevices/__main__.py b/myDevices/__main__.py index 642eb6e..14e5788 100644 --- a/myDevices/__main__.py +++ b/myDevices/__main__.py @@ -29,16 +29,20 @@ def setMemoryLimit(rsrc, megs=200): pidfile = '/var/run/myDevices/cayenne.pid' def signal_handler(signal, frame): """Handle program interrupt so the agent can exit cleanly""" - if client: + if client and client.connected: if signal == SIGINT: info('Program interrupt received, client exiting') client.Destroy() remove(pidfile) else: client.Restart() + elif signal == SIGINT: + remove(pidfile) + raise SystemExit signal(SIGUSR1, signal_handler) signal(SIGINT, signal_handler) + def exceptionHook(exc_type, exc_value, exc_traceback): """Make sure any uncaught exceptions are logged""" debug('Daemon::exceptionHook ') @@ -89,8 +93,7 @@ def writePidToFile(pidfile): with open(pidfile, 'r') as file: pid = int(file.read()) if ProcessInfo.IsRunning(pid) and pid != getpid(): - Daemon.Exit() - return + raise SystemExit pid = str(getpid()) with open(pidfile, 'w') as file: file.write(pid) @@ -127,6 +130,7 @@ def main(argv): CayenneApiHost = config.get('CONFIG', 'CayenneApi', 'https://api.mydevices.com') global client client = CloudServerClient(HOST, PORT, CayenneApiHost) + client.Start() if __name__ == "__main__": try: diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index cb8af5c..ab507ca 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -5,7 +5,7 @@ """ from json import dumps, loads -from threading import Thread +from threading import Thread, Event from time import strftime, localtime, tzset, time, sleep from queue import Queue, Empty from myDevices import __version__ @@ -27,7 +27,6 @@ NETWORK_SETTINGS = '/etc/myDevices/Network.ini' APP_SETTINGS = '/etc/myDevices/AppSettings.ini' -GENERAL_SLEEP_THREAD = 0.20 def GetTime(): @@ -78,7 +77,6 @@ def run(self): debug('ProcessorThread run, continue: ' + str(self.Continue)) while self.Continue: try: - sleep(GENERAL_SLEEP_THREAD) self.cloudClient.ProcessMessage() except: exception("ProcessorThread Unexpected error") @@ -104,7 +102,6 @@ def run(self): """Send messages to the server until the thread is stopped""" debug('WriterThread run') while self.Continue: - sleep(GENERAL_SLEEP_THREAD) try: if self.cloudClient.mqttClient.connected == False: info('WriterThread mqttClient not connected') @@ -146,7 +143,7 @@ def run(self): while True: try: self.function() - sleep(self.interval + GENERAL_SLEEP_THREAD) + sleep(self.interval) except: exception("TimerThread Unexpected error") @@ -160,50 +157,38 @@ def __init__(self, host, port, cayenneApiHost): self.PORT = port self.CayenneApiHost = cayenneApiHost self.config = Config(APP_SETTINGS) - inviteCode = self.config.get('Agent', 'InviteCode', fallback=None) - if not inviteCode: - error('No invite code found in {}'.format(APP_SETTINGS)) - print('Please input an invite code. This can be retrieved from the Cayenne dashboard by adding a new Raspberry Pi device.\n' - 'The invite code will be part of the script name shown there: rpi_[invitecode].sh.') - inviteCode = input('Invite code: ') - if inviteCode: - self.config.set('Agent', 'InviteCode', inviteCode) - else: - print('No invite code set, exiting.') - quit() - self.installDate=None - try: - self.installDate = self.config.get('Agent', 'InstallDate', fallback=None) - except: - pass - if not self.installDate: - self.installDate = int(time()) - self.config.set('Agent', 'InstallDate', self.installDate) self.networkConfig = Config(NETWORK_SETTINGS) - self.sensorsClient = sensors.SensorsClient() self.username = None self.password = None self.clientId = None - self.CheckSubscription() - # self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') - self.Initialize() - self.updater = Updater(self.config) - self.updater.start() + self.connected = False + self.exiting = Event() def __del__(self): """Delete the client""" self.Destroy() - def Initialize(self): - """Initialize server connection and background threads""" + def Start(self): + """Connect to server and start background threads""" try: + self.installDate=None + try: + self.installDate = self.config.get('Agent', 'InstallDate', fallback=None) + except: + pass + if not self.installDate: + self.installDate = int(time()) + self.config.set('Agent', 'InstallDate', self.installDate) + self.CheckSubscription() + if not self.Connect(): + error('Error starting agent') + return + # self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') + self.sensorsClient = sensors.SensorsClient() self.readQueue = Queue() self.writeQueue = Queue() self.hardware = Hardware() self.oSInfo = OSInfo() - self.connected = False - self.exiting = False - self.Start() self.count = 10000 self.buff = bytearray(self.count) self.downloadSpeed = DownloadSpeed(self.config) @@ -215,6 +200,8 @@ def Initialize(self): self.processorThread.start() TimerThread(self.SendSystemInfo, 300) TimerThread(self.SendSystemState, 30, 5) + self.updater = Updater(self.config) + self.updater.start() # self.sentHistoryData = {} # self.historySendFails = 0 # self.historyThread = Thread(target=self.SendHistoryData) @@ -226,8 +213,9 @@ def Initialize(self): def Destroy(self): """Destroy client and stop client threads""" info('Shutting down client') - self.exiting = True - self.sensorsClient.StopMonitoring() + self.exiting.set() + if hasattr(self, 'sensorsClient'): + self.sensorsClient.StopMonitoring() if hasattr(self, 'schedulerEngine'): self.schedulerEngine.stop() if hasattr(self, 'updater'): @@ -237,7 +225,7 @@ def Destroy(self): if hasattr(self, 'processorThread'): self.processorThread.stop() ThreadPool.Shutdown() - self.Stop() + self.Disconnect() info('Client shut down') def OnDataChanged(self, data): @@ -282,12 +270,23 @@ def SendSystemState(self): def CheckSubscription(self): """Check that an invite code is valid""" + inviteCode = self.config.get('Agent', 'InviteCode', fallback=None) + if not inviteCode: + error('No invite code found in {}'.format(APP_SETTINGS)) + print('Please input an invite code. This can be retrieved from the Cayenne dashboard by adding a new Raspberry Pi device.\n' + 'The invite code will be part of the script name shown there: rpi_[invitecode].sh.') + inviteCode = input('Invite code: ') + if inviteCode: + self.config.set('Agent', 'InviteCode', inviteCode) + else: + print('No invite code set, exiting.') + raise SystemExit inviteCode = self.config.get('Agent', 'InviteCode') cayenneApiClient = CayenneApiClient(self.CayenneApiHost) credentials = cayenneApiClient.loginDevice(inviteCode) if credentials == None: error('Registration failed for invite code {}, closing the process'.format(inviteCode)) - Daemon.Exit() + raise SystemExit else: info('Registration succeeded for invite code {}, credentials = {}'.format(inviteCode, credentials)) self.config.set('Agent', 'Initialized', 'true') @@ -297,42 +296,45 @@ def CheckSubscription(self): self.clientId = credentials['mqtt']['clientId'] except: exception('Invalid credentials, closing the process') - Daemon.Exit() + raise SystemExit - def Start(self): + def Connect(self): """Connect to the server""" - started = False + self.connected = False count = 0 - while started == False and count < 30: + while self.connected == False and count < 30 and not self.exiting.is_set(): try: self.mqttClient = cayennemqtt.CayenneMQTTClient() self.mqttClient.on_message = self.OnMessage self.mqttClient.begin(self.username, self.password, self.clientId, self.HOST, self.PORT) self.mqttClient.loop_start() - started = True + self.connected = True except OSError as oserror: Daemon.OnFailure('cloud', oserror.errno) - error ('Start failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(oserror)) - started = False - sleep(30-count) - return started - - def Stop(self): + error('Connect failed: ' + str(self.HOST) + ':' + str(self.PORT) + ' Error:' + str(oserror)) + if self.exiting.wait(30): + # If we are exiting return immediately + return self.connected + count += 1 + return self.connected + + def Disconnect(self): """Disconnect from the server""" Daemon.Reset('cloud') try: - self.mqttClient.loop_stop() - info('myDevices cloud disconnected') + if hasattr(self, 'mqttClient'): + self.mqttClient.loop_stop() + info('myDevices cloud disconnected') except: exception('Error stopping client') def Restart(self): """Restart the server connection""" - if not self.exiting: + if not self.exiting.is_set(): debug('Restarting cycle...') sleep(1) - self.Stop() - self.Start() + self.Disconnect() + self.Connect() def CheckJson(self, message): """Check if a JSON message is valid""" @@ -355,7 +357,7 @@ def RunAction(self, action): def ProcessMessage(self): """Process a message from the server""" try: - messageObject = self.readQueue.get(False) + messageObject = self.readQueue.get() if not messageObject: return False except Empty: diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index c775874..0e63d04 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -66,10 +66,10 @@ def TestDownload(self): remove(download_path) return True except socket_error as serr: - error ('TestDownload:' + str(serr)) - ret = False - Daemon.OnFailure('cloud', serr.errno) - return + error ('TestDownload:' + str(serr)) + ret = False + Daemon.OnFailure('cloud', serr.errno) + return except: exception('TestDownload Failed') return False diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 80aa74a..d77aa2c 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -5,7 +5,7 @@ from myDevices.utils.logger import exception, info, warn, error, debug, logJson from time import sleep, time from json import loads, dumps -from threading import RLock +from threading import RLock, Event from myDevices.system import services from datetime import datetime, timedelta from os import path, getpid @@ -29,7 +29,7 @@ class SensorsClient(): def __init__(self): """Initialize the bus and sensor info and start monitoring sensor states""" self.sensorMutex = RLock() - self.continueMonitoring = False + self.exiting = Event() self.onDataChanged = None self.onSystemInfo = None self.systemData = [] @@ -58,21 +58,18 @@ def SetDataChanged(self, onDataChanged=None, onSystemInfo=None): def StartMonitoring(self): """Start thread monitoring sensor data""" - self.continueMonitoring = True ThreadPool.Submit(self.Monitor) def StopMonitoring(self): """Stop thread monitoring sensor data""" - self.continueMonitoring = False + self.exiting.set() def Monitor(self): """Monitor bus/sensor states and system info and report changed data via callbacks""" - nextTime = datetime.now() - nextTimeSystemInfo = datetime.now() debug('Monitoring sensors and os resources started') - while self.continueMonitoring: + while not self.exiting.is_set(): try: - if datetime.now() > nextTime: + if not self.exiting.wait(REFRESH_FREQUENCY): self.currentSystemState = [] self.MonitorSystemInformation() self.MonitorSensors() @@ -84,27 +81,25 @@ def Monitor(self): if self.onDataChanged and changedSystemData: self.onDataChanged(changedSystemData) self.systemData = self.currentSystemState - nextTime = datetime.now() + timedelta(seconds=REFRESH_FREQUENCY) - sleep(REFRESH_FREQUENCY) except: exception('Monitoring sensors and os resources failed') - debug('Monitoring sensors and os resources Finished') + debug('Monitoring sensors and os resources finished') def MonitorSensors(self): """Check sensor states for changes""" - if not self.continueMonitoring: + if self.exiting.is_set(): return self.currentSystemState += self.SensorsInfo() def MonitorBus(self): """Check bus states for changes""" - if self.continueMonitoring == False: + if self.exiting.is_set(): return self.currentSystemState += self.BusInfo() def MonitorSystemInformation(self): """Check system info for changes""" - if self.continueMonitoring == False: + if self.exiting.is_set(): return self.currentSystemState += self.SystemInformation() diff --git a/myDevices/utils/daemon.py b/myDevices/utils/daemon.py index 0d7ff9f..a85075f 100644 --- a/myDevices/utils/daemon.py +++ b/myDevices/utils/daemon.py @@ -50,14 +50,14 @@ def Restart(): debug(str(output) + ' ' + str(returncode)) del output except: - exception("Daemon::Restart enexpected error") + exception("Daemon::Restart unexpected error") Daemon.Exit() @staticmethod def Exit(): """Stop the agent daemon""" - info('Critical failure. Closing myDevices process...') - exit('Daemon::Exit closing agent. Critical failure.') + error('Critical failure. Closing myDevices process.') + raise SystemExit From 106c35a29f30daf6289883944e99627bc3b08bf8 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 21 Nov 2017 18:59:19 -0700 Subject: [PATCH 093/129] Prevent queues from blocking process exit. --- myDevices/cloud/client.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ab507ca..ac6e07e 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -27,7 +27,7 @@ NETWORK_SETTINGS = '/etc/myDevices/Network.ini' APP_SETTINGS = '/etc/myDevices/AppSettings.ini' - +GENERAL_SLEEP_THREAD = 0.20 def GetTime(): """Return string with the current time""" @@ -77,6 +77,8 @@ def run(self): debug('ProcessorThread run, continue: ' + str(self.Continue)) while self.Continue: try: + if self.cloudClient.exiting.wait(GENERAL_SLEEP_THREAD): + return self.cloudClient.ProcessMessage() except: exception("ProcessorThread Unexpected error") @@ -103,18 +105,18 @@ def run(self): debug('WriterThread run') while self.Continue: try: + if self.cloudClient.exiting.wait(GENERAL_SLEEP_THREAD): + return if self.cloudClient.mqttClient.connected == False: info('WriterThread mqttClient not connected') continue topic, message = self.cloudClient.DequeuePacket() - if not message: - info('WriterThread mqttClient no message, {}'.format(message)) - continue - # debug('WriterThread, topic: {} {}'.format(topic, message)) - if not isinstance(message, str): - message = dumps(message) - self.cloudClient.mqttClient.publish_packet(topic, message) - message = None + if message: + # debug('WriterThread, topic: {} {}'.format(topic, message)) + if not isinstance(message, str): + message = dumps(message) + self.cloudClient.mqttClient.publish_packet(topic, message) + message = None except: exception("WriterThread Unexpected error") return @@ -357,7 +359,7 @@ def RunAction(self, action): def ProcessMessage(self): """Process a message from the server""" try: - messageObject = self.readQueue.get() + messageObject = self.readQueue.get(False) if not messageObject: return False except Empty: @@ -502,7 +504,7 @@ def DequeuePacket(self): """Dequeue a message packet to send to the server""" packet = (None, None) try: - packet = self.writeQueue.get() + packet = self.writeQueue.get(False) except Empty: pass return packet From 8797cd935026b2a6e492b79dee3e37a8e8818ff7 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 22 Nov 2017 12:57:22 -0700 Subject: [PATCH 094/129] Update property names and responxe topic. --- myDevices/cloud/cayennemqtt.py | 6 +++--- myDevices/cloud/client.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 3480ca6..ef7992c 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -7,7 +7,7 @@ # Topics DATA_TOPIC = 'data/json' COMMAND_TOPIC = 'cmd' -COMMAND_RESPONSE_TOPIC = 'cmd.res' +COMMAND_RESPONSE_TOPIC = 'response' # Data Channels SYS_HARDWARE_MAKE = 'sys:hw:make' @@ -157,11 +157,11 @@ def message_callback(self, client, userdata, msg): message = {} try: message['payload'] = loads(msg.payload.decode()) - message['msg_id'] = message['payload']['msg_id'] + message['cmdId'] = message['payload']['cmdId'] except decoder.JSONDecodeError: payload = msg.payload.decode().split(',') if len(payload) > 1: - message['msg_id'] = payload[0] + message['cmdId'] = payload[0] message['payload'] = payload[1] else: message['payload'] = payload[0] diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ac6e07e..715418b 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -472,11 +472,11 @@ def ProcessDeviceCommand(self, message): payload = message['payload'] info('ProcessDeviceCommand payload: {}'.format(payload)) if message['suffix'] == 'add': - result = self.sensorsClient.AddSensor(payload['id'], payload['description'], payload['class'], payload['args']) + result = self.sensorsClient.AddSensor(payload['sensorId'], payload['description'], payload['class'], payload['args']) elif message['suffix'] == 'edit': - result = self.sensorsClient.EditSensor(payload['id'], payload['description'], payload['class'], payload['args']) + result = self.sensorsClient.EditSensor(payload['sensorId'], payload['description'], payload['class'], payload['args']) elif message['suffix'] == 'delete': - result = self.sensorsClient.RemoveSensor(payload['id']) + result = self.sensorsClient.RemoveSensor(payload['sensorId']) else: info('Unknown device command: {}'.format(message['suffix'])) debug('ProcessDeviceCommand result: {}'.format(result)) @@ -490,9 +490,9 @@ def EnqueueCommandResponse(self, message, error): """Send response after processing a command message""" debug('EnqueueCommandResponse error: {}'.format(error)) if error: - response = '{},error={}'.format(message['msg_id'], error) + response = '{},error={}'.format(message['cmdId'], error) else: - response = '{},ok'.format(message['msg_id']) + response = '{},ok'.format(message['cmdId']) self.EnqueuePacket(response, cayennemqtt.COMMAND_RESPONSE_TOPIC) def EnqueuePacket(self, message, topic=cayennemqtt.DATA_TOPIC): From 5129b5c2be8b2235d1f0011e0059a46b93fe5f4f Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 22 Nov 2017 13:33:29 -0700 Subject: [PATCH 095/129] Disable support for config overrides. --- myDevices/cloud/client.py | 16 ++++++++-------- myDevices/cloud/updater.py | 18 +++++++++--------- myDevices/utils/config.py | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 715418b..60cb195 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -409,14 +409,14 @@ def ProcessAgentCommand(self, message): debug('ProcessAgentCommand: {}, result: {}, output: {}'.format(message, result, output)) if result != 0: error = 'Error uninstalling agent' - elif message['suffix'] == 'config': - for key, value in message['payload'].items(): - if value is None: - info('Remove config item: {}'.format(key)) - self.config.remove('Agent', key) - else: - info('Set config item: {} {}'.format(key, value)) - self.config.set('Agent', key, value) + # elif message['suffix'] == 'config': + # for key, value in message['payload'].items(): + # if value is None: + # info('Remove config item: {}'.format(key)) + # self.config.remove('Agent', key) + # else: + # info('Set config item: {} {}'.format(key, value)) + # self.config.set('Agent', key, value) except Exception as ex: error = '{}: {}'.format(type(ex).__name__, ex) self.EnqueueCommandResponse(message, error) diff --git a/myDevices/cloud/updater.py b/myDevices/cloud/updater.py index 7749d4d..d826ba5 100644 --- a/myDevices/cloud/updater.py +++ b/myDevices/cloud/updater.py @@ -16,7 +16,7 @@ UPDATE_CFG = UPDATE_PATH + 'update' SETUP_PATH = UPDATE_PATH + SETUP_NAME -TIME_TO_CHECK = 60 + random.randint(60, 300) #seconds - at least 2 minutes or +TIME_TO_CHECK = 60 + random.randint(60, 300) #seconds - at least 2 minutes TIME_TO_SLEEP = 60 UPDATE_URL = 'https://updates.mydevices.com/raspberry/update' SETUP_URL = 'https://updates.mydevices.com/raspberry/' @@ -38,7 +38,7 @@ def __init__(self, config, onUpdateConfig = None): self.setDaemon(True) self.appSettings = config self.onUpdateConfig = onUpdateConfig - self.env = self.appSettings.get('Agent','Environment', fallback='live') + self.env = self.appSettings.get('Agent', 'Environment', fallback='live') global SETUP_URL global UPDATE_URL if self.env == 'live': @@ -46,8 +46,8 @@ def __init__(self, config, onUpdateConfig = None): else: SETUP_URL = SETUP_URL + self.env + '_' + SETUP_NAME UPDATE_URL = UPDATE_URL + self.env - UPDATE_URL = self.appSettings.get('Agent', 'UpdateUrl', UPDATE_URL) - SETUP_URL = self.appSettings.get('Agent', 'SetupUrl', SETUP_URL) + # UPDATE_URL = self.appSettings.get('Agent', 'UpdateUrl', UPDATE_URL) + # SETUP_URL = self.appSettings.get('Agent', 'SetupUrl', SETUP_URL) self.scheduler = scheduler(time, sleep) self.Continue = True self.currentVersion = '' @@ -102,12 +102,12 @@ def DoUpdateCheck(self): mkdir(UPDATE_PATH) sleep(1) try: - self.currentVersion = self.appSettings.get('Agent', 'Version', fallback='1.0.1.0') + self.currentVersion = self.appSettings.get('Agent', 'Version', fallback=None) except: - error('Updater Current Version not found') + error('Error getting agent version') sleep(1) if not self.currentVersion: - error('Current version not available. Cannot update agent.') + info('Current version not available. Cannot update agent.') self.UpdateCleanup() return retValue = self.RetrieveUpdate() @@ -131,8 +131,8 @@ def DoUpdateCheck(self): self.UpdateCleanup() def SetupUpdater(self): - global TIME_TO_CHECK - TIME_TO_CHECK = self.appSettings.get('Agent', 'UpdateCheckRate', TIME_TO_CHECK) + # global TIME_TO_CHECK + # TIME_TO_CHECK = self.appSettings.get('Agent', 'UpdateCheckRate', TIME_TO_CHECK) self.scheduler.enter(TIME_TO_CHECK, 1, self.CheckUpdate, ()) def RetrieveUpdate(self): diff --git a/myDevices/utils/config.py b/myDevices/utils/config.py index cd49baf..5092ab3 100644 --- a/myDevices/utils/config.py +++ b/myDevices/utils/config.py @@ -9,7 +9,7 @@ def __init__(self, path): self.config.optionxform = str try: with open(path) as fp: - self.config.readfp(fp) + self.config.read_file(fp) except: pass From 40cfc6098eb60804d68d1906567997e70d8072ef Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 29 Nov 2017 17:16:57 -0700 Subject: [PATCH 096/129] Remove unneeded files. --- myDevices-test.tar.gz | Bin 79709 -> 0 bytes package.sh | 8 -------- 2 files changed, 8 deletions(-) delete mode 100644 myDevices-test.tar.gz delete mode 100644 package.sh diff --git a/myDevices-test.tar.gz b/myDevices-test.tar.gz deleted file mode 100644 index ee7c4601290b6350d2b6259b1c7f951266a50cf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79709 zcmV(vK?uehfeD=FM zRd~)%PoGuKsmt)FrF zJ^3T({mGuDqW|TpMZUOG5bX!;30L1w0~LIBah6=83s{eU9$2KE8e5cr-46BqX^A}H_M>Mi-)IcxxEO=&Q zZcQex?3p3$1kpW-;(G1ya8Ixp6Mr^jlIC14NwNrLWh;%`vUMBGR(0e-;@h38jJUDo2MOgg z2f}9oTmV7f^w-~%3a;8o8hKEe%i%c^RUnxXbvfv=sBlW3>}KF6pn@0URY>g5(9U+k z&~v8_D(od{+TH<6V~Q%?rF|v~s;#wx*gy%|1(ExW;->@!It4=#t%@jtI8&BY2S%^j zW4j2W2vOnTjwB^X5rqm@8}2q9`> zKUjw#vc{>GluT@!nwx3y@(lZ)iU;G-elMVk0GKIpzmVC!6nD_V!BNq(2oF52|HVy6 zGnmQ229*e$qvus-=`xHGLsl_dnN-d@#@m4tOra~K{WO`s zc(s2hqo$A?uN6e%eSd#D6<$Wg=LOCzF42yCLK%L}y9eX4>$9PHlQJaB7g1H%oYX?* zUbJ!inRux{VwR-w1pAC(}5!r?IF~(f4S)l+pV(o z5INz5;bNc|Z8BIIv&$BZJ$nX*=%9@wIxC*gcLtcytQ!KkI^!r?!!E#b7R=3j>mo`-(u_{dxyMekxl2f-hyOy$q8Tt0sQB%xc^48%71+g*JRrlt4Bt zCPK|tw_CQFmp8rU_~W1r0|c;{kO}Yu%ujC9`JBKsXF8cKoM;l_uUnW(;9u8I@XwdP zolQdXlnDaE+!Z2d(zk4)mBYFr=q@4APSdU|p};6V4?uxV1i$%gY({R=I~3vF&xUDS zAbK5>h!*J(FrvmmB!SV_BAyBeV+NFk65jH69rVGSyBW*}_4MBs zFYoLqD`@*L#1)XUR(nF#0$^86;%u8 z==6+L!4MXt(NyRW3^7V$P!Txl&8TtJo{ZYvOSAmM8kDeaZR)?b_c!i%Y-wQNV3feg zXk0?SGzNoir%5S8^((C*MOwFBYsJm}^#CVZE$E!q@nH+l)YLAsvLh96)NG(^L}#0C z+c-e5(i5dUFFaKdTz38rg=cDeR(P&7XJw*mhz-V&(Exf3eXEJi0i0BRBs$jawKxk$ zHC+%$6v~hC^7ox)o2qPb-G9^RA|}ztE{=h%_UIk31?8fbK$Tm~HO!aWL#WF;3xi>+ z|Dh*WicY`lbi0{BK)L95^SU+Z^~asdk5Kw5x13H75O9M!daaaC#=yj60A2$YDD}PZ zrrQS54BHR;*gsD=J82Y8*U z=a(B4&Ir`qy6IwHPlSY6_m#Suj(PkR(jGL9F?c?D&501zxPNs80+u`8Lmi(OMFv$U zi!tcvM92dHr z_8V>?CzA2>TQ|)yH>%TIF1=0cm_nLK?!HDx7eW#fmeX7pYE&1HDVwIo1f7&{)$E!aBeb3o_22=f7S&4i6d2e$u(BLK zBgm72s00Z=aWEx0KxtTk2MHww!qtTpq{j4x*X9LSOIz|9y6DJ$;%ZtvoC@Zs z@IQ*K*!THiak=9li7;J@Dj`gSlF7`S&r8L%*Mn)0`bi6Us^>qS*NT7?4NAF$X*N#j zDt_=#Iy|(VTQ*FMpl!a+o1?Wd2UAj&_rRg{x%o#;i1J zkpb1UddjMC`8?}8Uo)ROpzE2H1azX#WMwS_oH)hO(m6;{5hJ<*6>*7cUs*4|^MYH) zlLRkn@*-eadU}L{cD&% z!K8=cu`aBA-lLp0+R#2lme=B&v{}F~4be=%Q5Y&L%Nt~DNR19GPTC{ReQeF#82oHy z@jX=!eg(@V`~vAKUJFyK;9+tX1rN!hB%=d^Ly=4nA1*JzJ9uEMz~F?wWOo^a#UYra z?B~cRICcq&4;4n0@BlIQixa){f;$>_p@g-RBqkGl+8ATaD-V&IkdjR)GuOl!CQPm! zbvx%#8q|>QA|MPc|M2ba!9f>^5(jnjSTqa)GN3eE&U@fTus%}hO_X=jx z5P5|2;!+v6Be2hULhI=37ssn-%WI|mpFY0$Y5C%(+4!gNzO>j@f<|1}`1&pIiQMSh z8rTIyT`a0&aL*cks$Cp^)^Lfsi`r-2m12{_=k|sDTr~Q0d~yDnqH*Vf60XlntMh_@ zZL@+s7JumaJM`Sj(PbwT`lAj81Gd(3xaNFC};Zdbz@7ra| zHZu#f>UDcCgoN_rq}RA!w?R#JW+X76NohZ>XM*51N>4xnGVl>3*^^Fd6QLaHX~^wB z>jw#ST9a<)-A3tgkRK+rnZ^&IhpBW2R~!4oO=NPYA0+p-f8Czk47;0%WiXE?HOFP! zm`EPV4>?#0nAl*jFyhgGcjJ?#;_8<0TDI6e z6~br8>YN#yAnph6wq$!2>zGU(1w`(Cr$*Je!alrfT7}Cu0@1L5~5vCCCNUk z@LlxW+}T-zX%tp`!-$@Y6#SA(Ys2^zLPUsTW;GzJD8=uZ_JwixSo zSfk$Er!hz!>rOU#@L|{)>qFKDoWVVfeafi*e8#D8r&{J#yV})+)*Z`z`tgRxVbc)M zBJ^|!+iNxq0{p-k$M8tH1!^PWivef3E~L+hm(knMlfs$OUHnT?>(e-wf^sL%f^0B_ zX=&8$EiiFK2IX-)NuN?Gf~?5pY1F9j=d*EnHWJkSq?Kyfs`ZdD|b2jh9Mr4-zU6hi2gl zK8(RifeIY>JOB*&!EWO{6#+G3QN0Mk743eb7?@`%083}OaDCEqa1l&C?pM;s4|Y_k z&JUcGULo4tTT zSoECjJVe`F8n3K=vD)5LOo8Dj;)fE$CZeCw8Az>8dh z>fq-QNp?QTnmO^}HgKYub-S{h8J(gymQNn&v94QScV!J+Fgo0@to%c}Umo*PWau#xa`fL{lt1vq`?XxW%jY3Llu+CA!JFK14;&YdHz2E3Q#o zUDd$17_Wd^Z@s1{7CQ1S6fagG1@4k&%A0f{^Q)uOWahIi_on9U`c}QPZ9$ts zEOk;Z8MauM&E%(GV`5|{0qinN0GB~RS|q-QbbvK$%w`b;G(9(n)f1bzsi~Im=xwvC zb|T<)L>&2Q&(B=|*t^8Hrw=o%gI~M?FiDBDl2sKC5G5Fe4~T z%?;t3X64e*1*3OyV}Z5(in=;_z)EcJ0c0j01qVpwXt*2$3~hz! zuf23D&1JQuO@Q#Ra6P~X1BwYXJ7iJA>o~<~yqf^Ex2uFWQRJ*jc_Xza2Q*;uD;ZYc zdyw_a>B)C6U(8hkOiQdeo>hvAboea*u^_?UaqgR!n*^^k1aO#uB~EkU_;;Dy`@U4f zL-3o_^=9hPZIhw%l__?@tDaqG5Gz)!+{_gPEwmgdGxXS&1OM$ zbDqnx1v=TDg_k3jDbr)myYYj z=8(yugwzb%1BgK)X9|WglC?`2>%cV`Z#jsPgJ5oD=ffhK$EinA5SSe#V_A$gapy~ zOogtDZ9>YkKqIm|L)?Ny_G%+0&F#0KlU>+|PV@0C=rk`iVk~;Hf;7L$R6+Hl=Jkwu zq_IJ5g^w~Zw4vt0?S!4$9cSO=;0&>ISosnv_fn;JTkfbH^_J2`4NLwF4ck=BM-K+U z(g`D6XJ&t{*Q)#)466*C2=eVj`8bGP!!CVW;<`?;9s){=hY@4OLnC&bm*e1m6&U## zh?W#QM><|(zBh(oG2k4U^&n}%mp-@CDU>k)RonmHEx=vV==@dc7A z#{_EtB@xgQi77qLO&c5)fd|C7H$c9AwGJ(#pF->*H2McBhfwoc+Bh$QLgkPs2EiP^@w%@6}f3@~BW~sBf(n+?w?{3}RP& z>uN*oC);xpH%Vta)E-wW)uW?Y?a1ELB!G-IZ*y|GLmOv0O(SQ@yv#nTZh~bWj$)7> zTpzNvn4S@*a|#FxUtFAZ>IkxO+Ketd=paev4W_8Ii>+i@(Cp+R|GY4 zrw<+*Sd%~vd_57OHRF`Ii$&nw-AP#qe?kZi0txEA2`>q1cVLM^AA1Rp2 zb{!1RS1^P>IIHVMv(PHrb-)c!=$^JRsS3L&E4BWU%`ZjMjRr;|JKw47Ngy$GQ{r$F zT?iI_;=XsIl+Su_W5V9p0A?-FW6xvoZkS+{jCX6(rWa@VIG;r67E;Y%8NviAx_0h~ z6slFh`sv)6;@21T{(rImJMcv5IjNM()7S4DzZsYU& zFNgHp#N$XdB5jzqF;T?J16b$Au3j_;W3`C%s$Ii&%VfIIAh6SDkJ9$;fYsV(lQ*S(e}fY zHmAlmW^B{IxO4V_&(;hCAO3B*lC2KoY+HzZ;gXT&*NKS)bJ7VUOF( zVw)1VS6QicBDJ`=g%AHqLNU2Ad;m<(vgCb`^mgg8{fi3cMA>rwfA+q#t&t>0c)kw5 zBF8+nwyTWs!BxEN-laIYndVwdb#*Vx<4x=|Y2sU zQ`ngi3WY+UkQ53<(tzu&Gk8ef%dQ$hPdUQ&UqF*~QOT)MbIaifm|7J$S`TqrvL{%~ z^v~q6*Hw4eLW8B-x#nOQRYCEh=|uNX*N( zl`OO>~7HeO9H|U{LQG1QL+|ZWk=k6l~Z94?SXh2O1UL z^9ACbW$-NiBr9`HG#+b#%KRG;WE@PU50a%Qpf1fFOD~JsK4?0RLl%d-ip=$41c8V_L z9H4zbkMgCn7QgeFj`ZmGaVp2+!NlZ*V4ti~taC%OSWo>NLq}Gn} z2R1@2p1`^!lW8ldw$lMDI}ueHFUcnGj8dlyJQ}wOMn1f*U+qq&1t zdb3GTDiUfGr8t>X;s2v39XZyBU@a!16FWiDh>|_&hq#Gel-_QijEm&uWRgKgiyF4< z(TbDi*sQG>k-zGF1rnMfA?*X8$s~%#9z_jmqsGa29+2#2B-ENS2hI|#DaU>1N?1S# zRf~MWb*=zRl~Rtm5R2F~nNZ(7Zmwx$WLr@`?y1rXVBoWpT~y|a+v#&Hh* zO&dx{qjbL3xUV0M!uU2Y7>r%9GnGty9>lJNUOlhH<4O$`R^+vy>bL-RAG(8}S~ZWW zvAI=us3J!2qVgO=`IBj;oDU}}#x`PvLxsNq^+&1nJQGaLqy#VtGLy+%qi0Vr=ltPz z5FSQ|m9BieI9SZQy4)y$=M@)ro~jzBrAXDbA7?qUlL@EIQ<_ zAc>vkma+sShNYr>p*y@R#^fX*A2=HbQ#(QwbgtnKwd5~q@+)ISe9GGKghhcjfcb{k zYVS8}O^h8y?1ZCE8~`G{xK+2MIJ2zD^Mhu|r7cCSD&8gkzYWqUSP152?aD1qjPx?P z#F_-%FX~Y{ar>KF=(7ui*vS@LU&QFsg=Q|A;D@agNmaDHGZWT`ZZTJuWKf$mogWp) zY+EBkw#&~jnmh~T)P7OM9)LTi=>8FF?~YO*pjT7`yXSaML}Miw zrEDieRt$%0j&hbVBd+o)o<^3)d2y!Z5@)POxT!~N>e%Rc`&ea1sFi}Vd}wOgbX8;y zB|FoTsP7t+&c!aWOIu7+He++lI~^eJk2FPitC}&wi)IYMSTZ~huGMPl^TQelwX(fk zz55fJGAZ_el|v+L5H)ipKihH@M4=7&x@MEW(hNA6tQ8_*1EjJs8csqhOg5iUjUi`O zljyA^w_s$Ai;^7napO5}EUg0}j)UN?gD?iiU@xv<1*WPEZ#7UEei@{AqA*@(xrCf};@ZNccMGmVK5CA+MJ5^w2gBu@?K%A&n1 zRxZaNII2Afc{{)<18FT*tgIHBi?~|#NeHVEO{oZ{#7&tCM-IHz`c8GgMTB{MQ0@kF zb%E`p55nu?Ur*0|!9+n?G*E_d>gs{gSXu`b;TCx_V=D30h!z!bjAVg<>CL)Ro?X$D z$;!*C^8d@jQxcn$t}Ql+jq5;NJ>;utN)49w6E8bs*I-#WEK_E*0p_WlF)x30b;N~> zSb|HrE}o+wHAPU}W#U-3nt~*qe7i zt&0DEBYGCy1rxB}Py7An9n;%yJErEHuc`9XNcS`wV}j>6ZnfR3Rde+>Wgth zZO)#Yj`ps3Cl*EDXz)qMrzXpE#VESVrx;RI%3hWk^GPW*`Dq_V-6!=+-VE&Oj{9x` z2dNV)wTT9y{Z9?~YLsow7Gz+#Yotp2J?AvEirn z*$GZ#SPpx}m@L=46_Pd;uMXeNf>DpMcQ#hqcsK!R)}#`2{qs2Duh-xi-vTnR6n#rW z3{QOWd{M@wcuuA^E3GZ8SZbHkKZjzOOGR98VHNk;KYeZ0AT7vx9Mj?=ve&(?q=;Js z1ILWUH27ZvKB-T%dz;h&vnPmnBihDQ)Ft@xu#96gQs6AWHc6#(nn^LMA;@8s&BbPq z6kJ*dpC-H1cTbrCu55|(jZwXpLoMcBJEjB(TPwpdc#J~4`{VxJ$D{qcINNnB6qH>r zGj}d7dHhlDa2gEt&Qv@*T`|sRX3^!D)5b*R6&-7dc(asIdydI(5h4rL0#nVXspcS{ zOX!il4qnkG+w(Drm~9L4iBWA4CgD;volnY-A2nxh{y7Kkhy4rLPp1dMHEdcX16h$4 z5hDlF#tx6)JJKiOhbu$kEhCHyb7AGk(ZfHUawIgkMMuJ{x5x})(k&T?xlEPclm6pj z`{MNcSC;*=#wgOXwjAnRX0RVua4^_I z4Bdq5mz)jxc}i#GQNpwoAkqFDj`7Y0+H;x&bU$>DgGs_A67E)RCvYlS=>an=9dz1d z&g_u}tE^SiE?ihva)j9JD-k-}$Ic@-3>Ic0(k#oJ_I7cZmRr`)++JB;FOB9!ON&+0 z{GXbWR0^MPUvhY0x(P z6mz7gCFL;358h+$siGlb`#MuVshEi}XWW}+xaLV^<$~Y!M~g%M1TzuVp_u31%F_-^ z@Uf$z^rN{k4Y-o}y?~DjZZjV5NCQmF8sc$EExP)&@6&`*tI6jn^|Gkb#*UT{)r!Qb zlC+d|$k$DTRR4&ZrNri5(PZk0;yqTA>9>DgaXBG z!(^X;GNn2gi1i?C?~GN;+F4^Ao!bqqDVtCZ@rA%ye%D;UC8jEHR^I_U+nLI$ac3%f zRpAX7ZbYY=>>__JbV&*P=vz+-L!}hvT2*Sw8ECwq?1B~Y(9S@krE!@8D`jnh7I!8$ zaqgx-VU4vmsP<#L-upmSZJ1meS;;i1P}7T?3a8yYCZTe1A$}5xruLCTNvC1)nB7^B zw;3r?)m9jcj7ErWAn!vV&1HMRU5I@V@TvFy-#Z%cK0dH^v zBVk31ScUaBjj)Uf+zv$dD`f`C<`9&C3aS4J^}`;#jM;6-Q4$ zSH-B}@<_3f5&4bu(fI+$@&I;$5> zIPBpm74aSZV>lKR%Rk2!q80)WE_f+}=8G`=lLn-me&G~9hlFkAHP(1dI0)=>hpLM} zsvLRmm%-1no0YjmfL5KijJY;tz-JhHFu{75K#~rw8NvL?sWH9Pl&nPNJ#ymak001~ z_3k8eM&n7Y>L|J^m8-yDeLC1xc=PMZo8iiv-o=|Ax8EFZziGeuo5C#r9Y>=McbaGp zr83B({~Q%fR7{18=ZlhH$~tY6`?BeTyHSkYS!&WjyJp>$cQEA`owWRuHt`grmEyy3 z@X*~3ciXd<5@Gjsw$^#eu=_b~^YlT+3A@Y_jc(t3zoY@(Oil z7rgyn&M?&>zy~B)LQKpXz{;iM2uMrAWxO|U*4AEIXAwmYQ*Shvl=LSfJ%gB=~93Mp8{EWXOa1CxyL3R#4O(mUPHeDdU5 zNZjW&sfKc9)5c6q$|KI5Cv7wK&&w=$!GCd!j z&A+H7*NFsi$Tt6iqVtR<8d&xq<90kltUODiYnZMRew9q2_b~E@QSZ92P{%PV>V7+Y zV>puxf_ao}t+}VwlWYa*jo`77Ll{5vb|=NI9?K=7zhfDHaQkrqr}*+=9|O220n7d1 zL3d#Z1_j^-)kV+m<8cz-8-^oHU~v~t zrAoo$>je6yLmy6l;vW1fx0Fe@!DT#vfE57v`>l-De{i=Ph z1|!OhRh&)xTg7s@dK=vL!kZv=B?8hhv@I)lIgRTQ{@63`T+xu0BdKobBfN^84L)ry z5(<2`BM(pXvY8=QP>=dqrldG`XgYlkf-y+V*>Hx}tB!868*$7#G+kT-%NzQdl$DtX zReCVRN8Cu`ig*Q~;Nyz3VTYH9ba0j+``&A$dC3{DdSfAIYbmP}X;S{kyNzZOtPcwi z5&VW-1Vy4)tVMA(6Qkxv&7Ivff3ywD8$|$ZyDV#*GREg77zGo5kQ7nw55m#<(-S`Y zLW&i-BSB3Uk9Njt?|R60AIA@mZv!vU-dJ1vQ*EPG-&}9d-&(EyCw3Eey}rI)`%}GH zU#m4YH){C4zOlZx{wJ^Yl#b>7%uvVjyg&Wb{~WwTdw;M`e-aJ7_NN3)eKe5? z(3j|)WQpo*3QUjpkpMr4qLL*mB(*DKuvVBvsWADAI?0sowqgLM6r_YRlfyVain^cq zJ6IjXW12~B9B`+45KJ)}87tcs50;h`)0lfZy0eM0ElM5e_(|nO)*E4LYJW*Y#cy_+?!|{$st+3llxX$~6=~jTKbUrJ{lOqrtM8jI|I)I0oUUDF=G1h;&n}K1a@}au zV6+ij16qvNi&zpTA9|Mr8zu#CV<}?ch*VdqJvuTr1EQ0m;2pr^Nn#d3xdU~nl<-lx z%!-)>h~)UvOIL63V{*ja`RSQ=vGd_*pVlYY_c>4`K2`KFhgrJkyo83q3!@*R%iU@}!@;#)FEV*8$ieA3K_$xV}z7#2p!U@++M@T9$e ze&HRST%0#HFkNYrQy1RvC{^z*jmPg$YPop%Fd{1LpYeEy??=UYSAa=r2TRKoRwC(HBV^B1}k34 zpv%vW9qBw{v+xl|ki|lu+1_R&CxB$yd8s;tI}!YN;CF-U$-m4?F$WGV=p-Gh-obwi-5FTh$F7oG2)9qsJy&l5Nq z6Z%=FtTrfn0qCyUn1sC-nS2_B_V$nVVV)hFpB_61#HJN_a{jfLi!Lyg!d}I?m~L_M zv{`b0g*-GdDRllTFIh(43hbT%@2y8Bj=4A!T3Hf0(TI()-+^a|>h;GN`ma6#mXHQh za$Yk05_$q&_@cu@i5SyK5m`+!?+U=87yG|lcxUH_$2;f0dOz*|TBvxJMZDLQVG{QE zr5lA`*yo(>Gr`(P=Q)x#Y)y&PWpLO6Up$9u%7!gL;atC*fU=ij)_MbwV^tUqK|C8! z)KF5@RQia>AmKBh^PYS+#dvtg!q?oft6bFqeinFToI#UI&$l z=@~F_)`HGAsb$B-b*-@Ag|ZKZ2hh#RMRB6D2aHe7(75R4>Iz$)@v&g=idXFMt)dj@ zy)ecvJvpo9&X^QHw{oMX z;`K-SfV2I-@UGS{{J)!PjkQ<*?@N3P|0)f8l?}Rd^0M`2;`YU|=uAola4lsy&~LNW?Q= zkHK#w083-E1-#gxBKzgvELBt)OSxN=n&4V#nSy zcaSFjs0V)paz2I@@C`0@OOeDvURI=BUSy&l#nm2N10-s(OMx847_Q?e8tBNL!ILF4 zSbR`WKTh!AM&Wik9mm_Nt4vVD)!{==$j@lFiVE^-y>|Se`nPagSdudn2LfvwiYgW_ zEDf)5pS{*tdyD@pEmLF?U03G`03OAkw!Fp~?V!g3sXhK)zJ9w=ga71y8GqeI!BA4R z$~qm?Qac@al7}Q_79_LoG-%|)_{@ga<(K|S1~1(T=siCSTi^Su}A?>#c_dQ>SokWXleA4hZS&kE7d9>_LLAo*QLVC(w5M2LWsbl z3(ZX8g|$o&_(^!I+MZpb-E35*WD#g78?B4*3rWBnUIB~ZS`tg^9xaK9;2?D%QVV|^ zn?(&K&`$^CWe1mwNW*Pj*ruP}G|`i`MZwIQQ7FnJxS0)LwUPWn&I7clM=Ym-N9(My zX*8S26g@n{aW_eCT-7EbkqU~_$z;ciq71CGA+hUv6Op9hz#^#-6LEq|6F{b0hGc&g ziUWZsVelous+F8|x`V(U&CGcF>ugGQ;q|q*;^{$Bl!s_~i`R3<)3~I!@7OCq??8)H zfk}ryjXJBtYrB?9qG8?DZgm<>!K&pl*LepKE}*3h!mv_lHx(^-iLlDsFMo@$;V5-& z<|;cFDz4svnZL(*g_0sE%&>A63CL52cx;rC;yk~r`-%z2dQ_859>xMkIPoaKK+GcqR23k-OYfwPSQ5AEs>r`nmO_lp*5~ki zLO2H+Ca11|q|n_M9ius?GQiujC$-x6$8l=YYJHVy;6n-@TX%Vd+!i%e$A6~ktY{f( z7`IGKr{&Ig+~)qjDkx_~KG|c=%qqyLMOn>1En8-F>q=}qJ`jLu8E@2{tC_~jG}E-3 z`PSCi*Z*YO|1u@uSB(E`)@x1E|F^MGtG(L)FY(E;s4Hq{<}=2DRy?RoxjK361h!x} zQf2t}Ov4@z+)B3v9&o6zyR0kTU)!fA`*gm+vfr6MQU*6mD_(3*EM&K}<-nU5KM$(3y=w(9k>Zpu%2FF_p)50jaVo zc0j~CC_IIC>`2%u`W04t{=@2B5PV(*#c_JO`VjaNQCimCuJb2Y5Go)DP$;106pRD> zTdh__Ei!uXthK;`0$Q;xQ6d=iGT%?b=^)_!n+Eq&@toav3ojl(5RM}8nB7Yp7q;o= zCH;G~wER1)>&rV`+VDy80CG2pIOmAzhdDEr6vkBiCS2#@ZW9=BU7ZPIx{`6!C2pw5 z$x;1lbnVun%yfh%1Qj(mgx@{~58FIlh&-Oheu4%GK&qWiFYHb`9rauuFTk#eMkI*t zbc0)h!50=4rS&2?+O_N*f$@PZ7Tlus7%4e{M0$~lAI5Ae7tck}UFW6_rVzSnlh-jq zpq4GOZlMgIgH)EG(N2JFZ;M)qejzJ*rbmM%&Jph~i&H%%Cl&hd z=-TP_;tY!6)JBbDt~e`-iQ;^Tp@5yq%(^28?TZ9185II;hnlTnkaSN0aHT;OD9wYb zpq@aLm35e+C-Yz`Jx*ZC$_Y%&ol%rCKLskKYgNTV0y3#;3>OnXTG&(od_hq3Vyv zFgr@@r+!!KGLvu%eb<^$Xv3jLX)34?trb;!JKSLKpFxYPb4UJ0;ve&mS{)A^IP*=BH>zJ{$@Pz6mrd4O`mCy!x z#WMo;ShcEA7z?%ZIH)A!igLb54j+aQ(PeC1SlHric1RMXnunr})@aay=m%=K$^~sG z=V-q}^hd1j6Yf^`r?{(5(Yj%mYwDn-gRV8`(dIgCbq7XPBAK;cy(&|>mJUZf>PAg} z#bXFsoL2hFUC=B+8{Uo4~z~X_kecxmS7#hW8+YJvdXDMm)gW6 zxCvtn3DBpGeBUPD<2lVeu5~1AESodqMhu-!4=Z1-Gp-Lm^J4!JhM|3r^XN{C zWlT9!iW%3bfwpKdaLE|SQp2T zsfegu*00JHHGGdz(B2)gKUoY8Ouv6H` zs~U99wl<_62 z)-cb0FrAG#{iIh?1uB-dat0{cx-PxDR) z2V>`*6P#c;3p8Oi@34IqtC~xQv_Hv>FrSOLPe$@%gvLR+iX34I?!@KcPQmBKBgGC* ztH%sfsE7W}lsfbJ5P-4pP@T2c3`SD>Rb`Q63n9vU#V?O4Yj;6p(Qq{0`8qI+cVVUc-|$1r#avTya(#CbWk`i`pOaer=i6at&}y!RdMgzanpX*!79} ziJxjbo3axk=y1SX*M0P3-TBPhHICvqyk@q0OkuSiiw$P(tqHSzdDc3$g&1qlo^yiK zB)qwuLa94s6W_zAG=GAsByFutdH_x~WAfaI0z1VzWZMPhROaLbbkbi(J#yqOugrcI!{Qj~BaYu05SwbY_QM<9@fHT=KSc|N8YTgrNge z{K>?Bpjd2ntqMA}^hKR{F@f5BO5TPJm;i@s3Y_GBA&+Iq4i4>opwUMc7maeLr*?)p zz;2h4QMvNoh@Lt$t`Vt-&Q*xoM!(JQXFhOBgVv-LpFp@{6voct3>p@}*+>Ote(HM+ zT5Z0KptrwFjyYm*yq^mACKtWVXCp3X`EG>XZ{`LOb{9J-PR{18&iB0XQ>eD%21zt> z-W*AzWk^ozuCxq;g80h-;IiuG+AZ@8;Kce}W}(K3B}-8(DLzP+-ow5?nXQNn(%kh> z-QjTn`|xnsDJlx zSnmh98%c)7?YDHd;0n_fpInX=k=5QBBHV&wlQNl%hpG#OPgqw~?)4>qOj28Oz*u^q zU^J9px@#{^;Up_z`QpBYWM~JVl{*dv=%bfijA6Gk7_b3U<>6pN)QHU6DISb6ysw zJd&HZd1Nz9O-V`ZT5p4l=$=Y&(IQwDr?9#Saj2yePoKXQ5@M@7!wYt$c;WMsT_r-Q z$4f(m(U>1&Ryj4{QS++qR2Q*wLytCnc|fM=my4}{rAo4Q8mqRCwpz#hurn9aEZnkM zRmv|urY3_Vp$`eNwv=-$KCWXpl(?NUiq=qulhxV@WcldDv*J~SNSvE749eTQVT_-1 zPHOicErS`O%QD@rGIuU}nalY#^I2o@=dT`zB|=Q4l}`5w8<8l^SQrFJ z-a8$lSwW+Y-5ihlQ5`yTt%wFj@32!jo=axJO(;vyYG=%xD>?50N@Qz6BF`eRunQIM z<0!ZXlLD71QY(#nh@>^nqq*?h7Q2Hm^|-rwu@^gHLVJB9ShSatX<7Cr?Q-KheFEF+Y_R##4snmtYcy$?317D2juh z*|Un*28*;im_pp!6~c(o0L2c&~GD+S&bafA=R3FOR{ybcqgCkrHL2 zR`Fo2moZYF<)vA{&4NLIamfvSr&#Po6A7(-w7-8w&;+g@&yTRn+39m6!B+KNELgd? zlq8H?_71>oJbRel0uwf?HFlu_WlF=>N>ZdM&u$@x7@kI;%ZN0PhxAnOmo1~tHt??f z?q}Sm;7v4&F^AVM8)%r2xy~Z;kjcn%x85Ss^i;fP#FLYWY&iTH3q>7dSs&TF=a;{s zmP3;#_WA%ezExtqmxW}o^3ymY+BuU=OnnxiP|}H#Zd9|w4aos_JAn_Cu==Gx&Ljt4ket>Pg3qVC<^45Dk$^(C*FFA^3u-)OTZ zp}#mFLpZj)0=8GsbzhaSlkeKxlq#~dE0R`OHu}yhbP8^0_|fL1@*{zu^@V#qL~0s4 zA;oq&bVn)-3C+>QF7DFG{1@|DQjP?noJws=Le-52Q}#)4mISONX-VJKYubh_3j)qd z8+GIy=n7cIw31d`ve`N*DNA;`S*b?~(7xVmGJ*NP7hM#qMZLf%xJ!R2l5MbTXy#i^ zZ7VfUa_qvH2nw(ImFY<^imHVM9ac=Sg}JrM1t$|!EYX+di<@ilGV4)Wv<&l7#(oMR z?RYPTq)I^@*;oqETU2`Jav^UqE48R z?M5;uwPbosth;HxogCn)Vszj*ws)DHr?q02Sty~A0C_-$zeY3dwqfy+!|tVon3tFG9G zioHn`C4n&jU^nUo#FWfj1K~zrgsXI)O}X7@IE1CIRKPU5-k4O|rnllq&!Og?E+{fh zPoQf|4>IlCv)1aAWB0%m7@~@)7nPUNI&aT$9J19k(+7c(GKM0LMn9+k_z&PZ4GC>5zIw*WvFT1DR&}pbAJBfY_+V@4_M=c zXz0#zYIa|7@GCl^Yv(3W5;1e(q@=A#6|Y`Vc#*-Wco?vd>_q4ja-KG4tgt#^jDcY4 zmu?8M{Gp1YDVx1SmbL>J@lpb)qCj{I&vAB27)d5& zMO_|A><&S4BtOkuO-su-r2S9s zu*3YgWm2kwcpQyf3`@nStb_;J$K7?2>P55Zc$RXQmA$LpO+uL6qJb)9moB@etE8cv z5>FeMG^q0<&g6*bYmD2Vwa&e!$Pno$sd$Medl980O$2nAYKzI;y0^oq=e@i45?2|v zB8!&Tm{d3{Oj}aUuRHUGRDZyLkFpDyQFIFir;Z6R=L(-2->-i0C#!?#M%gk@HYr%4 zMxIib`nPBf3^%qVO5B~It)Zez*tw2+57)uhvsPk0Q2sDf{(TTzy7-a|018X z@$UuQXyQ+!Nxb^xHgJG9*Vh*q|Mg}a#((`8`ZxFUI{x#Ue6 zf1Up?@u6)n7M)OHnN?ebANKM3Lv%s63I%rGVvENC2tDX-f~O-UrT%QxRn#2(S)G8I zMXA^|!+>7#+^sF1#8rREZ!)5&9PI@UBiGWfais*`FDbt{UMvZ-Fm+1(;j~qF6Bj7X zVG4|nDPvm&%=x=I9{Ay?kh5z{Sp{4GqW(q!%=TZ!uZ1uk`rS#Sw#=x8PgQ!Vpq@DX zIp+V9m4L<9|IM}9=Ii`_iBJ0Yv%k;L#^US$+WN-&>-zr^pLydC)-_uEPikZF@!zO5 zHeSd7B|bUF-|xls`}+OkmVi0(f4#nL$^VVb#;g2)i4P`)*^R~zEFVduR&T7pKTU6c z5_Y|_ss9-xs6eCZ-eM%|(lXt8a0ILYdR5PxjVMvcZQ$*U{Vx3BZz^&VyHTxqB`i?j zuL|Y=M=&0u8K^rCwDC5hO#T!-N~sIr^GE? z=n7^5QA&FLl%tIw4D{VzUA?=ztNMg;HJaS44p`B6_2_VS|D?UY0%-F}A4daB&wx5| zIDsx-KX{<9K?RC}9r$;+)#2Yv0&zvVhko{Ogfa0^e|qOn0)m5a>yz-BRO5tf0!Qe( zS^#f^A9;nHws+VrcprA!hi!HV#izrIA5TACc%OF8&v#BP4)@#M>AAOida`$Tad>(H zzYe^elV81`4o~(fUJz3BM{tk1bTNvWZa52r9;0462s9)xR)+FCi%iY~e{?hRZvyWI z6Gv03XHZpX%LlXC_@f@d8HB@d$}`bue=jh`;@lDIC-Pc$$ zYM}PP;Scg5OFe&d+PV1ge82r8d&nZRe>~J4HtC@Pqtm|Fx%k-b9PhM$V$inKCOW%6 z?wp+LAF=oNdQ*G-!_MIeugh`SS6?5W?(OTZ>j?sm81Q)De}S3Xz4c*180_NJ1n7K^ zqrr@w{xI^A1DVVaZW^!>0a85qKvc6}dLwJq+WQS-Z@j|>?%2nS;ElI$OG$+lb03{& z_{Kl-g|aEaw`Qp*?tq>1yxi=hXqAz!;x)>4Z+;m$fVJUs~S( zD!`I)aFqRfi@@5A-Jm?fL963wfHr)6x4OcJ^K*_iC2P3bBuOIg(r~P*u491T>2R!HeZ>{05|JGmq|1a{f?7#I_)9=;vdtTE|HN?cO zLUp2?hvgek6}e-R*EUQs^h6nPO;es5OfhUvu8k!0+R%q+lA7$J;`zOUx$jqa(<>yIQYhfj89;54)abpy zp_KOJK9U}*3tW4v(Vd}~R@A%3D z-yX9vLAA7%>Fm?7Ljs6e`2SDnz}Okx@WA@2^yAbV9=E(iOK7t2!j@Xq=uO752EB4D zYm7>n%Niev)Md>_GUbxW2|*oV**z$#SmtAMu0g6=qi>>lcG^zxTz$hFJLUddjPG>ubvYfxDu_1!UN{^EVi^la6xBYH6DkeT>I>Bf!GeV${p{X zHGir7Qi8e_e4cUt`fWJQ3QhPj3HnwP1b}r%K_Ij;*fkYztK!vB&^d(WOasY08#Rj;0jWMTbmAGpI$T|zCr ze1eiFd*2V}3Mo6ad8stHtY3Nmq7#Ak(V^de_a>K(E7cZ2)~Eq%a28pNGo0MOA=!g# z*RR(!7%CAHTFjv!UX+1xNfj^oH@O6%$Ab#$%9A<^GeD_Jz>qeZ_InwJ@cs8*qYMI zeE&VMK&b?>BqeHNrT$eJ*qirRrZeSQXH$6HSve#R1{Q*4ZW{UnSjV-K;vAOXU784P zL_`tjc+j2NEUmHf+Sg;{HG`Gcl=Su*4qIOjhb;q#EjJF0Zzh5pFD8PKV;a_Doj2?D zdzTnqL;%kb?X?uqt_fgmq!`CGp1>`Qd>q?IA8_W#;u zWBsex|Bbc!hH3w=*VbO`|Cji@+W)Wi{~z4`f6UpAy`AK22S{?1T_t#?p6$p8b=`2U z9JvoXKucyp9&zlnoW(s+SB65#GU;eYe`oHzdFJI^U=jPhF75|Ipe_z-Cum7>u|E9O+-}HR%Xo{=E>vI3P z-2Vf(`)RpPR(p_C%-s*qAMfseyrW<9^*fY-^frMJ*S7L{`LAc_uL^oCJ&7Q_!xMh4 z=XjYFCg5z@*LTnwNp~;T*oC;r-4o!6prj&PzWQ3b2SCimb;$E7jw)Sh&4ya2{x{IQ zbct6-lt#GhKj5-o_sWF!gMv0fSgBX&zw*^WzFbQ?QsXQbsV;eiD;q9pHf^A_3ET?n z;NxMYOEUaQwns_2aa3=Qr+${v9qQHBJ?Xc!fFS5}U#0?N4TE=XM$tsM*~tX5J`YIK zV<%7TzUZV-_XKnZ4b)J1F*hEWAA%BlTPyWC=AeqN^_)_|MV1>-4UFSWfHW{6WvH00 zIVTwjPyD7Z(|k2qpWpi2e+G8a`ku{JB~3hOE=D)7EcPnN$TB*$tnMzkfu*yVgp|c< z3ZzVSlSW6$y(a>^u~t^OWggp=xE%wylJg(%M$7QD!e2E1USc zE`Hb8??$8S99x|LjKQ19*rXJG4z}0KMRh4>+kLpC&-3=`K3+0c(l+_Fn1dIgCnsX* zLH%nqhg-b`4&G?5@m1Pui1r$dIfri-u>amSzK;FZT(6t?A2-+6UhTgZ`MmmnUk$>4 zeuEH1*CP!=a@D?o5t!;6e!8XSc=!CK9;5P`p2OhdIt=gVJ2u&UV{zU;J04L zANk3t|Bbu;`;D!yv;W`R+_e1v>#yHnorI?22awqyF7@YvS?F zA!j^#ye=LCpo^XJ3;uZ1+#Yu);DYt3Pp1t>-G|nC?J<)_l|)T>|gd&HJzV|&DhlNp;a-8HZIl~+LUyJ2&Lk!DI-SV-7Qa# zJyHDF!Kg=*tasVef>X@yLb(+JW==VAY<9kR@Q5xZ*2J0>y`mZub@M?(iEm+w`hg0d zX}mrFo0HV~489c91>OBm3+R9St@ZcyucQAp*6jNqHedaJFY>YUzqMD@?^X5tqqzPa z8Twh#a(kcc9-!=6ApUc0>)GQ!*U2%M?&0hC;=)pghLvAaU_H&!6byKC5`tW^uIQyI z{QRo^^eXH4jNjwSq_&SW-_Zt2ua|t5Vfm)HXI|vV{aUgWwpKPRr1#_nO1M(D?`N0IsFA#?Hl%UZWBsIax@+*Y9uSI|^9n!>CEHsU zF9c^fy|_k|>hR&I-)i1>%8eH`RSZqY4}srEGuWR@qDiSB6t_1$RPHF#0d7Zomv4I2 z>YLsbZ6_zmA}e+aqzAcrK+UA2R#UO((RVGA`twj{3Qi{C{K^XhZGc_Q%*x9bUkYQM zliXM#M9L6b1WQjlNv$UAmaFN6rE|1@uCshLB18W^7Y0Dia5x*LG#_%psKn3FIL@WH z8T{nr3nqahJED^S)(l-Bxd`LIlW2Mf;yf6F<`(pb8FO^#jr36VR`cb}HnLvNGlF=4 zhb2HP$EZdYz59+VDfxSqFHfW^RrVr5Ckj<`Y`s-(=0Ael)NbTT$?Zs{ox@1AmJBf7 zTv9HL!_sNNXlW4SZ940xQsQkYbE;Q3vxVP9j-A24oQ5*!>9^nIGb8xPl97f<%i~+Z zE8$B}xFEDRo0Zl8%{Q2msYSTaZb&TeT=+P<8s`a)UvSXh@v z)cIO8;A*8@UdW}k*vg(H;7(iGWg6n1dy&_0RWQwU)+o;+Zx&?AxRRDz7BVU2%HchK zHn>;W3_@AinkD<0O)(L2PSb_^kHYrp-FFFsdA&Ep z3+w)>p(@PlyedJA>3Wxkk!`5P$cU8L8q{2)odVj`Z4-VTa;AXSMXH|g`|Q*4qg)iE@^2@S!Fq`_jQ{C(&fY0?<@Fw7~ry{ zPt1>}%ApV}8o%@??*V|%<%bgElIa5xh0;7w*jR4&lO|l|HJi`zgzeAwIa%aTQ4q5Py!cp(*8 z%(DFYaTeXRQdyd@GP(1p#U`;9BFj@0V*Boqc{Gv)%}|9k>6y}3LkZ3>vU4(+4q{v{m3C>atRo&&FIJVWP5Q+ds$N~dw!YiS)nLa!a9k} zIc{oRL|Io=^(}CH?`=y}L6sLPEr&MgsAwyQN}*L(Av#-Obf}+1+CPQ*UYwX-N=6i}@OYtOVjG&8aH?O^2 z$TN+|go9oe49Z9Qq=mL@JEWcbsq8}}%`m^U(vtS-lHaZ7@NGkx6nK~Xe!?_|xHcOQ zOfH9b_KeIqa?VQz7g`lHsQNX6tP=aacYgTu{&|~`mjAdc+y(;>WCe;iz0|Kxu2@&> zRkS-OE811974x#$igrP0#k%9PqTiZ!mEh#};`)94o<<9B+H2Qq^)*6=zOIX}_2g+C zo+=Rd6<{2KxF18gUqDIoujFu7QmN~}FqLfrjQ!FJZ2j_!QeaEAMrm}E3^zu#_YJk$ z`$k=qeXp10XPS9ge#%NOK!;vrfdlsIpDHGGUi|0vEPmXfALTRW{0}@b&wsCPuGL@T zKVRbW8vpqk|M^1azoS??3C_RAv+MCB0#OsEoECz`@+O#wD9&DRJ-eyUW#jJRUpV9O z@~7<%3Ui=M1;3x~w9h^q?woY~y5DXe?(5G!{kXsXAHOawefZcuI&5Fys!q#%;jpn= z*ycaz0Xv4$gNNjhuOx47#$kkIYuknP-VUrhb@;M-1Ya8BNs~SKU8yyUdNW`%sCZcj z8{L)b?y#}R)=~Tk!nnZfWT?bYYM&i0iqhtGVRs)F!!7($gD>y53&)4;Q^XSRJw9z8 z;#Y?8PpJsT<4_`q3V%Xk3?#A%O1ix#t8F#c&CYz>Ilox6H}$pc!o@F$@A`J({FlP- zI>^(L{ZEJI`-?&apnE$PJE?uYtFKVeE9L#2A5P(A|`a zRlm42oIDYz!eyu%0WoyQk4+&a5{NN-jX&Hje(AZRg?g$0$|zmL5tFR*+m1L zP6C#6e(hJggD4J+r_^2C&li;YPq70xmuxAVrjd|ocUDIglCo%_!QjIz7SRgG20AHS zeBcyBd8QnTj!@cjL`Oi~6Uto9EF(P2>)*7ZxO&<--}`i~v}jD-6S`VXyd*CJe~PDH z**E<)YXOs4&=>tBHcK8C|HgRH(+&^QGhz`(#u0jM_@fda&?#A={t7- z@jeYU?#mW-{SmT00tT@qy+jPWx`j_HY2c=`^u5^tuAZFx`?3FGe^9<@rcwoFA@G4(yc9s+~$mNfk`bp`=^N#*@9Bl8YII*sOOMX^eon0L z(2qZR{H}5SR=(2`Ew*9ms%AeR+VlR>@{mEDdVC3!l_Pj^tKb(0b~(e3v#`82Zy?o? zBv3kn*O>Q-5E4)9NRn&sUs~S(YK3$izI5%($|)z2>{PZXU_GH`) z0GeY6KdN&mexNPf)w{aX=-$MuF~ZTOHHezYwbOk>2;jX!(u-7gx`U!gkC{@*NU&A? z@RTN*ld)FndiaOSTzy0b*?fBL9vUX2mVQ+0Dz*_A=Ll%YDHWM;m3?K8x4qwfFIHha z8T!*waPQ9NwNlS*p;{Cpzp-dr)`nKuf3j_rTKo({(rIDm8tCzKT|m4d?ml?qhv{uJ zYI-ZK1)SOzC%x>U`|Wo=beTx$?VFn*93SqzEL-d-eGT(RDWO!#F#RK0`tDC&`ww@$ z9vcWUC;!)4ZEelA|6cRIzR2e_{^QjceDU}X5YS_0v(YI@VJza#X^x~{?rdKq3p2rcPH((KKAF&w)`26|L$wQ{aAa=^bhIqK9 zJZy@GHRa)&cvx2+u8W5a<>7{S*i;^}n--w2DoR_d14pNy)Ta#l9}j={QNx}A__&AT zXO+h1d#9gH{;c*DAAUU3dP~pGcVO^r%AEXgv~Q6{B(F=Jqpg^`XJ(_qLD2fi?l7g(kSei8$%h>9dBkHcVCta!!Ab&*Uk{>ZMYk1H$`cx5j9sp3B+ zUag8f8AYXX#Y==`1*jNJTU7Y${Pd!8zQ41lW6M>G({^H}O(r%D%nE>i_1cbE#QhbT zHzXK1=b_fA?G!#n+(l3Z6=CwYjr^Z%l78^xK>ku%8T)apRy!O4(}cY}P+W+c9}R(@ zuzPg7A+>c2Dl_6S%EYBi9b}sLhv!as|LT86(q1EykcLJ z@F7_Qg@Y`B!AFDWE|}=9DpscD6~a-$IM>8#bBCbfwo+?>6`WBGmedS)m$jw-arAkF zcLEBbR#5J(5h8k}u40c7TAXCw_g>wR35(N?)1a>GegoA(wT8*xEBz+oD%l_u5CQmw zf8ZCvfuHpE_p&vL#Pob9Q$~@fp$@vTTo{!s@aMQ>JPJH5Hd){a^H_nc0!3k5((2-> zL8Tm0L8C=_i`1ECZIO{DrNvkdjnT{vLN*3MZ-Y;~n@h$U`XWjSd#OOZC7r~3Lm`;@ z6qTqdy969l0831ErI!YlhBu7iuDtJ2AxD=LL(~9LmhO6(X7<{^mq)|rEnDYp%nN98 z(2BTfaaz$h{nOA2m5x1DT&_862WDOp8j*iS(3B~u^McZn9U#bZ0Z>IX&?A7669J+9hq&fi1)uu zCrO%>ckoC&w00(lS=9Jl<6#oL>|}@>`^GZHP_f69EmtdVtr$f)d&uPB!^wjQoz(n- zyQ@>7V5Re}vvRTleQt{Igc@1aV%@wu1wT&a!cP`OQW%eM|7uTI0t#0FGv2)`*aM57 z>q+2$w%%~gJCm+J$Nrui5MY7_&!;=A!IbR=rXe~c$1{@e)(44l9Rw)AF||LHr0GZ{ zQ@~506Y`>J2DAhq7l)V|$zo=^>8o7us6NkNWfGP-_*nWP@-Q)zw*Ryo?4m_B`!a>$ zm38y6uIiJ>6+hx;9KqTa-z-!4ES1fwVljlgNYiEp=T3ZD**FM3muePIPw0xxmRGD7 z)0mopL^rR#x>{znS>@>TQ(}Z0hgyg{eBC7uOI<=8aS)3J)O`J6Q>wUB%#88HE5c+9 zRNWyW(gu|2KQM0P9K$jPNUivICL*6y)-an+t91CwOvQ`}lUKqeCZhv*Ilgrs;ZJL{FWfmS1O?ttkRUCxV z=^!Y&pnVT!Bq8c}_b*~sMts!lFt{_;3P#HZ;43LL6BEUa+;^fN;iLSb1^iJYG%mGOhCj*k7dpU0j$%ZjDVA9gS zr4Dc!&Qx33Dbfc&hiFKt+@@ojnYrcqVq@pawvzeSnkwbro%2MQyeiIl&J&nAm!0+2 zfXX|Q2D|Sf-u(4mjCqG0aYa>59unxAk;3z;7C)Z+baMLXq-gfoD4I}>l%&)?38Ci} zU}^bUxdi*xF^aFBulLCt|I2b3ewFi|8=LUmxc?WvzQ+H)#K(&NU4ISzeGUEn$GQ3! z2-modqCwuvf3xF%$-yD&BoRhvz~J8{d1RCoXIWQ8(ilb4*J3Eyez%g}9tdh{q0+xH zVwqv^t7%*^?@o*L-CngpDGX!L(-LckC}h}@1dY8$RfkiMSJ1V3FRLc|V)sBqr)n?2 zxIb31d)IGs95va zqe(uIzWUquXkl)IHFUxA@OqaEON$EhH{HYS_=LwK1OzxyzHW( z>isc@m~rn6_i1xhp-kj$(B8VYC4&y-Z8=MxgJA^NXPyK~2MSBcbdDx}#7;Tdo;Slq zM`Y*DasTs+{WaobN2Va7JhR!J+DLmOZlh^(qcE^q8FTecq7!N?U{a zuh(AB|GvoQ_5AOv6X1oN0BOd2nr$97x}w}+V>hi-JPt*v_Sxa0=X}NLzI#Se&OG&c zc)E9Z?$J5V`x@IgJUlx+^28%{cm^t+T+GWhzac6f$VZzJ*3KD!v?Y)@m8IU(qtm0E3;Ad#(^tV4C-c6cwBw;#ab1nP z_D7#%JuZda6uhb?t5UaTc6vQzQx19(+`xngCbE%M&0Y&dA#qh8Yt#&z+0yT`6W7J1 zwBx*DPLBexT`xZ1u525LuGqj616$35q(ts)S*mpjfmc}iC3Eod1@@^B6f9LjRB?!1 zXljY0&Ut?bppED=W{LXRD7&*s^>SpB>Sbk<(psx{J*Re;FGMU41xPMEanyLc2?e28 z1IV0!R^X7Ts{ZjZwY?k(Gc&w>C5BRH|EM4rooKA=R=`OJ@70^TUgMRb9qh44Sf>u7QT* zd~@t(5M6^Ruk|O9%wUJyD_dpZnB>%^_jhd~%JSn0KGa}c5{xTlJppm=Dv^>p(+q*5 zI-Q-pz4QHcyYsQVzgIBS1ES~Ip<8lLA1YprxVO$@C!Qoohy-GlBkYo%MqOCX~>7N=fJ7@eCfh z86lsP#nEcL=_PYEUEFexS0%4#$69_XWN9c(N-gES4@EAOWQoY`^)m1~J;M3NDpzPy zURioz@3LXu|KMmk$8`v)IFL}#z|1`-kF!B}!6b(&mBhN#&XSun7_}_<*>+7FMG(ma zqiwBHZLvzd917-|)fQ{kS4JUOOWM{7)~>(INT@i_P<(3->Ob};um7L!!rfeZQ~= z(0kbi)~3rJwp{)WuGAKIZ`5-rE>O)a1hr-!&3A#?$vV?wd*MF#J>HzpZ2_c}Ao5Px z+xD2sOY>Wxx;r#dh@KMv zGT-slRHY)XHAgtS+6S4Ry!yW&@>huesn;5tX8aHQde#45;`6Hizv}=0xQBrFRRDZF z5qh9FkaKvVWuccT*6E+4(@*#|$y^VrW$hiQ)s@BP*oCX0A zw_2L`55%GfbQ{3v4(ODD<99;HJ+JAZ&H(Bq@Fl(@qj#l z6f4{iXLEzyU9oLUe_hG(rJ%R7<1>GW%}EuR7fEN21eBdJe-11<2)3L8ZG1N@7#x!h zPO#Ygj5wwb?LeP==BU~eF@0PeQ#;S;O7mdkbBd*uEF&qH|MMjGQJ?FxA}!I6671&C zEDUuhZ}J~gCVYzoKq`C9Oa9|viB+BxY7P@i=WT|@rfC_9i*Al+T*icp@#Lmi|HX-J;4}ac2Z?kE|e_R$=wweO6 z<)v|$zhWQCt>{NREBfKcihi81q94Gk7-yxg5+uZtIgW-5a#Ujv)rdefPEQRtQR6<; z_zN`-LTQ%o)E7L~tk^25C|f;B>0V4XjPt_m-cxx0t=&?upBMVf^Z$m8?qjcgm7h8B zAL|=+BmYw!9=*nYyvXO(|NH9yeWCkb``yuWkP;2@g|iDU`r&lve6Mr9|MOw{(7x(b zgnV4jq$Bvl40On4k8T?Tk+t#*#tu&h;&Sr1>UkG{6Fn@(>_V#m4-R79{lT68fV}|# zvr!!0j9>%?5sn~d#XepUKpeD!;Rqev!ERSPm>rBZm?!XK1%8M! zU_~r@7@ZFFli-8Dc%>e@vZx#N0`Gbfea7`J#@oz@vUGfbeK?*?hUZ5il(aaPq*CYL z^n7<;RNBa@1dQ|rsD$~s?E5x#!V*8?=Kcc_2f(4g@p_XGrpR9yS@%yq9Lno=sr5Bh zncY&l=?SVp!+&Yiu(YkglA0J7pG?++6E7t_GkOMo&jWw0U@km%&`AhiljeGf*Gej zP6w@GqsA&ynWWjbt4mAS2VLrot>mB!z%D`kIdKT~5ftO>iRygdCkUwG^Bxr8Mxn?F zS5sTn<)?jxqfxh>9C3l}n*kLCAPbrgQn<2vxKtQv2TSm;<^skgph`lDxd-~aiYI53 z7A-Z0M(hGt;ItmDN&te-I2MK_@k3QSm!|0O_ZzvFyt940xW^6Z&NtWIJlVz z2f{r0kczoXw~LApp&ajBb}S4v!dsb^B~7K*GR zDM_iTESP#Pu4jF9m@teCA^4Mt|4_m-?Ns2Mk_Wv<*UeCMcg!wup+GA##L;*Yb~oKOSD{Bv!(NmdL^r@inMEkD%k3TL zMx%asBMR=)uQ>^y<6si{gNoRYfi)d$m#V+CR)n(D2+{DEzyAH4CDtyU-(RG1Va#m^y{BZr`CYU;kW&$hl z-L*p5WcUepGA!WeOesXE}A%1WV8$xL7!pmk$F9@xMsUy~Zi4QJFqS<|q2 zSZj)w%{Jyo0df)oL{}ND(?T{+Z~ZCxLP33-A=ywi@GD~#0mzi}Vl;tOXv9Qck<}+PJ6DbxFU{1VQbX@#rQ&d)SGITE(IL1VK z>l#U|)FJ~SK93uiJOiYb(Sh9c}`1O^3;;8T&@~g5Ho;%Qp<^q5;_C>QyrLddD$Fdgwa5l?J zp<6=0Q`Ht5tnyaDNj`HnqSv8pL7dnz^WmV!3GT;!ibcPORN zPtCP3DeI|~mg=3iRuo&Sve>zB`V%hL)8Kx(3M;t01e8Krc@ryjQ=%gyy@zT$N0=Mv z*UtVghwY2@TtGD&&N?Bne|~;?ZdE7NoQ+Y=Yu(cM6FGjKXUW0$Dcmjg`a$2i4toqR zZdBX^y(FR+>ZxpxTQm609VO1@apBr7o~6Sm~$*+N+!(BSp&tLnZjA^&H|Q@uUl+MR`*s&7VF@2g$MGI*o?! z-o)R_ebeARjHixeAh}1_o}&^|Lo3rRon`2kdC)k@qt#+!-71{ZODq#IlkrzI9uLB4 z396JYYgeo+6GfTTZ2P+#jo7w-gc*lTLx|&;ZhqZcq~s(pAbEX19Ly%cw)4tePRp8u zlq!B~Ta_)H@;=|>eWLVgoiaB`t<67SDl%J2E|+9rG2RzsVDN=AN7*RTNXoC>MNV{> zy-IZj&C=VqR9+Uf!>Lm$S~>?O+)j`7GG#46F!4YkzhqakO0sX&rHjHp#2HOhPvzpDw2SK|bP0P^1v_ z#O1`25&f(XpWy{h*{$a++7glSwM5BwXdZHSrRF19NgiIs52JYKeMfA|KpSQQGBd?l zmy^cBD}my+ak)1(cnSaTOdL8FziZWP#uH=2&G>YQo~GGw)c}10e}%Y^w-{$I2G7PB3(;W7{W&HH1*eodE(@RN%KC>4fDz zP@EE6J?U6%WYd-v(L8wcTXW*^C#D~d9t&5VIR=K<^%3BewVbCs4^xRW!YE%w@TLZ3K(a8$A zuIQ9tfs3m^s$kUPrE{I(0yc%x$fm54dqf-!WT`V|aizB`LW&XJ@{Of%r)IX$ttBh` z%2noFDP-ZIRR*qELDqr)SWT5_w6RcS3-`L(?n)FcvauqmutolBTL`S=J(tPiy@aop zP@ERqny5<(F;9tXh6G>o7WYP#w_+re642^&7E3hh#A${gW0yJ9lv*OZlPa>~dn!=w z7#2X;PkNnbCQCA|P3{1S@JX>+UVbKy{H!c8)z85Ll)RGamazA%)lvGGvLZ_qmf-Sb z7ckQFQDPC~3|$#j1V*~oR1pYb!E!rmxv3|7vP=9hJ()dkV$*e7+c?!rh`Gbd zh&>jgl(sR#iyE6H=x5L#@uB0hS;G5Wvvy*NeoyQhm~X9tKfLZytUqswsc7FU_VD%d z?LYJ4zj$QJSB(FvZLF_vnDJj5jpl3o*Gqg}&;Pxi|9heHe~}g4Co}{7AYX-?LdO(+ z;f@Dns-CFwc4=wu<^rHJWepj0jm~s@-njxZ;J@9H!Z^#<2tEydb0oN@O+=bm*R(b z6&P#vt}Yid7(F~BIrQtI*pNZe^uv38?EJ-)JPlS6_Z2U4#vN`VXlXezMsFE0G&an}3Eyjj= zY8Ou;VaPiB;_?h>==1h)`$3!Es+gcj=1fwadH#)2pC5sH6K5WOQ^;aKv+Pv-B;I?4 zsN|*Z8{VLBM0NYz2QOyEe~#>_q0T;MgsbFWHZNpfYw5+`-WsL zae{Fq(=m!llVTu~8JE2+Nk!P6q>LIiyG>`}c^teeZau|eBIDxiI(*N7I)qG3LHLhY zc9wKXN_Zxbar-1w8X{!D$${QMuc|8tK2^>AF< zT7RS$XrBLPW6eDOx4DMn|LXtw`kz<-&#V9Eh5SFMheY+9{+K`MkmZ+r+KpgLbcuo& z!4PowVLb-!JVScGH{qN3aC`>hja?4rEST=G@CVayI_m}8PXT1|2d1mSafMaweseQX zCqy%5DFHkIot6m3@7C3;f!dUWij%QayWiOmGtR7Z_YBv!rU4uBiO1zq-=zHa3SWNxy;_y7b&Dve!wr7MWhFVVirHisifSye;qV zn1aQ)B+VvXu9i)7MRBunMTC`liT6wI`h`i_Bq@x{aZ|fLSfkqjm4QVuT4kqUYt>ra zZajTH9BdeOOMg6D0Ey<>^CNK+Oh2rfILxLR(JvG~z&%R2QVB||NEuM}-bx8l_Ex>d z+qd=gqz1~ll9+98>1`d0l>pW|F|Di)(98RVIV}L+pMt@caFjYT2;gooh{IXjU}a0% zZ9E_i4>+sXvJPX=#eD>o5ooR1f^yNiez%-~Q(^0DmyHSaj=-ul77`!Jh;Os{ZPz{So_uh##o_5aA*|E!!*FLNn)x(ndP*)Z&d z(+9B_{P+OCKtI3hMWa!7cmPB}E~9_mIZ~1;H!ZEpeIfW(wlhGz8{E3jgLVv$10U=u z7y|7?LEgoLcWz#qQU})o{>Mab}xMgnl*Fo-|hKR-)Yih($RGd zk2284#@F)h3gwax?tQ%O%_H99oQBy``hFMbG4bNr%?(~~34-Cy4>4bLZ5Vr{z1f7O zi;z@9!ilbp(>IuwK|EsR8=jOIX5jpNW6hA@C~v2J;A%(It>P zZC+7B4JV9S6NbRCR;N$d1+ji*!{P_z39kwRtz->YZeyol#A1(S^}!oABZ3)I9v_xq zeL7RlW?Dfau35g#cGUX;Mvd^S>uJf6B_VSft9oZN4-KD)S_l;z5;Q=X2L}i4qzA0b zTd%&c4w?hA2Ef8VJF%LWGI=gnQv?&g)lIwSv;-(paU#^(FW#|#zs)cs$w-Kl9n^S6 zllmDP2|~a=%i!E*4-P8=W0Q=`LL(w@ee7`DN`(J_+vxwWpPcp|KL#*`S+%$kILH12 zyRT{7{|>KT_kUmH^Sb~0Y5~5`{%`z{dH}%4pUcDS7Pjh)P;-1!;@>r^ZqfL2j6ZAf zNLTgnH-~MSb6R9^g`Y++>^h!PO-uVhFHpp4*LmbM^4`RH5N=7fQ?b;&m)p_c0i9C& zP_y6ga7HZR3}xgov#4}egHew~%@%&$w-T#{BGwgDZ%T1mDzuE?Lrzvv>N0#+-`3T1 zmHL!(4ZgCym71!2szkve&#ogf7Ul;?N(w9I;aZzHv1ST`*6!<=xJ{f@QTz%C4vofF zMxoKD$b&IX3_g5yDm1n#$uS%^71qBp3e67{dA!Gg0XyOIm2p^OBf(D-y`BpGKk><_ z|4)bGuebkGUo-E2+1%K8z5n4wKCk-!tN#BYJ3#ZddMYmv`k7XNUB6Lf_lAplJM@rU z>@D=Ni{rEAMoHTtQj2AT5%jw9E_h{ORx4Za18S9p?E!}A&fac|GHEF`0l-zvf~*F< zvT-2XzS27QIU0Zt{1#PikUQ(R2Y?Frv+LN&VAg95YtwI@ZUw5+qc-#w7&$~M&vxAl z8gi-?$F_Bps7t!8kL{K4z{@q<6e(foCGM`QmCLwqa({4eU}2>MOA?rhS8tTxg3Yzj zSpQ1a6;OC{y~OC8XS6Gr{W==VP{y_D4W&f}!>LoNF@>Tskcja{@<_9HaWzy*%Pqw43dHs?>$l7a8hV^CqH!VW5Z7uG`XU5 zaksW!HcS3X%lqHj{YK4v{`Z!Y2D0fD?>%j^t?S5{L8`TSg+sA=p~ehD!5Re&FEAT{ zSJcU80`g^IueJ#IX6m%p`|`}#@w zf(sjh%bb$CbQcGhJ^VLD6u<|WZ?)D~ZO|p~hp?E1{s8Db04@x8 z!8$LvwpxS#n)vG=yaz%7Dzm_Q4@)}@3OtE%hlSldg5v>eUNF4~@uD;RiVyX(8%-ub z7YPsMX(Vi~I*VACCJ#FFWKlThNE!u9hvLA}p&SFTd)_3v1{pQNaRV;xex_{6{+B51 z0jGQkX>tKC{wd-%^J63w7dv~qY(L0~wTC2<#gokwS2WK}w1xlQ`# zwBnCu!*CSE6tgK*sP<9k=|vkox0uFxzW)O}KDjtQ zJ)*~8ZvI^@s}?#q{J~>|>P8{i!r7fA0M8;zlVqqzlwzZIr{iZ z2cCc2XBCz*6+(97s@RxR$WVBUU21MfnrBPzU}-Fsy=9pk1tZeN(OrOIuovBdjOv4K z%^snmh*9HM38(*1hs7shiD81hDCbv9PG8_(1{6;w4j}Qt@4CP%nS^RAdeg9N@)BaLx#|3or$CLS(X$($4M4$|?3Y8SA{Z z5o9Q5Z)1I<{!2;WUuD`}93Jm?)@qH;aa+uxhW@f%t1B<-#>+MKPMv+S|3s-DcTP_B zk2R{Nro0}z$PydZS5tAl3a4& z29zE}8W34kQJ`WsPo9+3q4KQR`vWw^XD_x|HXTSuvwNFkySFtK8|yyDn{cl>Cfc#y z?rnV`2z3X9TIVjBpdU-3gWf5ANH}?>+|h!q$~tvNow{8I&q?B%%JD?Pc80UTG(@#z zBF?HJ?Ucu|wpf1o6arY?*#J}+PF4>4t?W(3Q=c(xHIEVl_?K295cv9|dz0vGvIg-x zW1JIPmReF}3GoGEDdGz)p*TKqq$0cqK4hE<%Gk_HUq74G-Xh@ z!_4JzQx^l!UK}XM$|V=ffARiTM+IHE8v1#cB51v0wrWHm-6*W1N{pxW6^{zIJ7@IG z8SJcan#B_0!Yp;EmTR4JJ>{-lb_{CdUDnrtJJ4eZ3*6N_hC{$93@3Sx$!`^GS)&ED z3{wSOSaT)ru(yebP}W|1{+JR?pFc`lfJxrA*ixRSt!AU)t$JH^?L2_m=vwYJ*A{BB zt~M!onnO-nQTYp-S^Boq7g5lrFJ4jY%btQDd^urOq`5Qmt z$`J*eu8!}vOG1O>_XfW8%v zR_<0^*HEd=O!bs4tObc_K~p|WU5V;*$3~5LY$}EaN0-nnp@v?ArRc5JwppoVaid@2 zm8qmTW7Gjt^(SSp6ycz{gh<^~@dWYXL{7!N=6%;Pzou zqPK!?*55bnH*ZTC@V9U4RpneCu{k#8edj`AV?70lM&o@75{>uuG$s z8WPPM)4aauG;gF$yZUbWIkWeIz7Gnmn8~!iGaG## zq3d?cW<9X?EC!9?LW%(f6I&G%?wbe!QwGo%+26JGX5HaG%b0zF z56@uz>Lcjfvq%zp%-M1f^4yh%G-V5*q-6_{c}mqZBjqK1m7`@2zs!#nR2JcyF5|I& zrhFG`11`II`^^_^ziG9WaJO3-q|F&$42Sd zO%%bf1XDjfL=7)5^1Zug@)`Tkjr<98H;6&XvfOLfbEH498X4u)ta!JKD-50|rmf=b zacmwNTj#nOlWs8b#7g$_?#U4faWhsfX`8QBOdDlUbIHeupF2G79PRIQc0Zn<@1I=A z0!>+9=lDbCZ0CIEc>iMmT!!Lot!V|$_uC(jE;>JgDklo8%K~TTr@!bWsKBOP;NX1! z-#_l3?EWg7c&``O+579q_J!Ju9V{S?_2bh$R7Vd_K$qJ;V1??9_Z0BPya+^UN@Wq> z2EjD3$|=C=oKWaAkL!)~AOC~3VZE&%uWzu|!dg_{Zf@0o5^oze&(FTPzAQ4yL;d4IX2~k#dwf5GeAJ5@oZj zj=Vg9rayMoVcpRyuxCc;r0=qD^2knT8cd}$2iw_9#+j7b8D59bcbT*EIO^%ag!(N6 z>M=8cR(L%DQ;>pJQERwG4*?249lng1EKC%cJ%XV>;DHdArh$MxejVqFIb52!+ddh)G^C zO3PDj>VeY!3>YQq_Yn^2tFWRoor20X^|AoWzd;qG*}xMCW1J(U^>QvU>kDWlt@nyQ zD&ct7XmXroa3SaBQ+ljTET%VR z@C`WJ32&f_?)c_=SmbD>tFLb~D_#>cnYmth*;2Y|z9QZ8dZW;Sqeb0cZ&I~|bsQdU zmGDv)S(=mXlC)t1DvQDvI@>(Z*uh0%C)#52nb!`IaAEo|oi)GIm%3fWfSx5hM!_A; z2}69i-QapNWq1CBn@mP`_d8c}np0hWXZhAO>Zh}wJ|=zK1prQV_{g*233hXUgI5cK z6C7rPLw;Qt9A_jO8ggZZFTd~Ek?HxI_QS@7Di)_&UlAo@~e|;JaHlLk-;Oh z@pKYF;ZZbN!MwEogzb4qw>0{&yZ#7e&UGN8u#}Z>nN{$i3LZB{r}hwvRN1@iJTG?7FWFs zgbDxo-XxmcD1A^|?t>`$JR5sc+D5@9M$ui>JN6%-b8$2bq7gzJDUAWuzJxuz)?`TD z7{mqT01?v{8Iq8`{TW?BlUSSff=J_!y~2U?E%(@NeY~t;oh%)L6YL()v@J3-pyObOze3*5YYyF zm!9W_`3y=QE;rAj^(EUExz)aed-AA$=I)rL`=!Z05Pjl^F_(Nsql$89zNuf?uD->p zQ|)gZ^6>gmJ~`um#17V1i2tcKo6QX~|5u~F{u=-D5}()n&#%!ye~|poIm#V({@Ol0 zNsYi6apg!xzu@!3kvp!Y7l5_qPoqhUDJ!R<5_vh}DJ8rdLyzR8GG|Cb(49(8yDFNN zZ&6Tgmx~9Q#SzA$M%9C~fDy&2GBLN21`FVQMi@2Qd#2G!;D_`nvD+8v&pSF_xUmkuKZ~?NQjWZRyIyUq zuUq~ty#W^l2=NcnuunG^->qmp5!j~*E2F*1}rj2U3I9y9nn8?k-q+=5MnfX=QSG%6DDXpJ9)GO z{RToM!9;AlXx>-1oM=5eU+2QB+0bxG6_C?J#;ksJNynNg9u+cYE$31?EEV{X{ zP^p?3KP3g+qImRk;bBaJ%;+dpRF>5hA_<*bY=KDXxz-2ge)Al;o=Ta4Yzn^K=OPWX zKbIy9$K-ZF?jBx@45F5c2wB^&kEMlhVy_N|xd@TqFqa-0x_LNO;?m8B%P#bCJ}x@a z=4CfI2$PR4k3plh5F+6y7bb_J?(JfTU?9NSBAE22`Q%9yWl9o(ED@uz2m%kiTnPN? zPkMO~Xl|%dOkAYC*yI(pT$4Bb8u`?zD}iKF<(5NUCA-5x7}F*YeVaFd6_oQ#Ux^1S zJ6-6-e1lOmaKgN5Q8*7c`>;BJ6d|vl|UiW{#_WlpwS&3rtt=}C@ z2jU0xde)uF6V?bHN~Z1cT+ifY(1%(4_&pt<*6!iSzk7IK^wy94>2y-!ZCAYF0nidd zH;QH2*WfRz;xSUPcE7$>|3Gk#JD3L$PXVI$fVxw~v}&c|VX=Z2sc^-uO|rBEc&Be6 zv3|A+D1cX|(}$ho9bCt+{aFvsb+z7aXgNiAY&)w}MNu}5ajKLU05!!IYY@u;mhAN# zx(ARwOtcjRwz8QcFmUZaiFW`PH?zdgauSZ`^Xo+lA6V3hC}MWR8Vag7Z@cK@D4vaR zq;Y<7$V%QgQqmM0W#h6042gM2xA1Z292FQwoeFSo^|W)o_vzgGJNtfex_fc)Ytjla zb(Q|*r=5Bg{_vpF-oH3F;y_OJ_K$Xyit@|yH89O=j4UZQZrTOak|@GKCe@-bn{l}c zV~W9ne`8k0XdQXk3@aBo%hj!NU1zz*6sl)%+Pr)-Ov!!-z;Ott{AOT3p00Zj<%?*}SNk1CTrmVaLbq%5F zua@y}NqUWSyN3Lda6+teLPfs1*lq9Zo$nl;R0PDuFR0~$>+nRsAo1IKWlc3iHDwmo(ABsH@ICF67ylL`vJ|Rhi75jil6$%i>Cp6uZ)4 zK*ovzi6MLKDIR2YoJL|>ZL&9I?|X9(P_-@*RLQJTrlqbI*q-S^!HM?SX`A5<|HniY z3a%6&sUTaSOcy%J%0hrIQ^ODl78R)mBZlDIxPzol`PE>1>CasI?=}nuz11hRfkxiu z`g)rEhac1at2NjEl8g_sM%(?9s)qXZ!Rpd4&5({84?4Jpu!X)r3-wB>}Acl&7NSW}LBe>apD7~;dL;9DQaO)eU!bgcxGV%=2KoUL{tZyrS{ojXZP&K9f3K&LX15= zJ<=XGSn=c2z5Py2JTKM$4z_dhqOQCUz{|<&hI}oWfa=NXru{kr0+Xf}1u;o$OsWsR z>1A7FZWmXX@fSz!51j)T@C)%`i@#`oIy~Q3dRDKA8mFgcYL^@0#mOl&qr6xXFO00; zn46w@gG4{x`QdPvo=1~jsrb2AW*jWv@3-4KKkSz%ON%vfpGq1W^3Jf5NQm>m?|G}< zCv;2TS_5TyuHa;ds?~(9Wr@}K9z9ao6)+^8G#bRbs7bv~=PC6pL|Hu(NyHFy6aP0T zJ1E%MRetUv<#?(Cn1N@5+DmEzrtX>+rj1lgd5{XFgBt{+8(8>D^=kD*qa%v3&e{HX z=hNx=UOIkFlaB6s7Iw{f*om6~hWwGfL(K{VCPXXoC&F*oSM*Vep1pGP7!onmU=A4U?Y_O@78e4-yPkbO+UE z?8s?6-Fb|-id1`-4>g23{CW5=v%2k#_p zLDdqr$EE_W`5cXQCOK*`k~!ij>B1g8BDe7G|9*7z$+0PP(Dt-=X~j)eWnl*+Bfnz#eJ1|%$YjyoVPd)`H=flPQ!|44!g(_E%}hc zNJ@NbU)vw?Gtd7^s|C9s?a^-FIs1RL&4%g!-E1~r<9}Y{BYbWT)>#VIeR44#O6^CO zmLC5N@FXbA#OUTCItT~B(o%>^E5<)}I=Jo@JDs7A*$Ikl=~(856w974jhMqX9C>kf z5{?0W$na`WY~aaHl;4=v3l`JJON2rA`0SVMQ$g)$GS=wK&|6sY_uk8Mq4(k12d zPns=@wz$(<>P!ebxKE%GsY^w6_Lh5?Ww&I9g8TA<_r}hf|4-WZ=bw4||MfNV{@0C- z&DZ>oFY&`oXk&i!1m7I@9s80 zE&n#8i@HyCE~wBr?0$|JWB@pU6($-&VS)%|Krn%IXaEZ(Vlf299EN`jL`3%zE>gGX zV#MXsi($DM1ia4D(qW%ejyr!ul>t|eZaEo3E!H3_nP5-t;MJgb>j+*8JMwN|L7iPM zEiLVw9eN*QSQ?jF;=}yI-+%wTFqXs=lFx?^v^OH2SMfKuT*PboQQWH1|B4DoQRncC zeJPczQ#{zm-O?wtwTzk%>qWfnt+DKehiApuV|D}wi}zq`dJ(%xs~ZL}^hZ+x*Szs< z6bxsRf4c$eZ9s}2aRY7>(w*#}?NmwmLQ@U2BB{Ed`38fw3Zw^-A()BK^0hVm9!hWMy3~lvIwRvaXGVkCqZK2`jhT0e@g5p-gIvxJSNXxfe9y` z^N5rV+M7#DXTLUo0s^ZK(M?-wvP1yn!EOG;#)BZuCWAqEUFAG1fL_nSL9c^$C-6_= zdR1yPoJE&99no|LkK5O(8_-AE`exjwZFMWm1YbI0E)!RZ%|%%zy8iE=i+kmaM~g-J zOMJmfK3PZ*yXFxTyx>!Wg(rjs0doip(Mk87s(I#vRq9Y{E$_0(Suh+|d%(W;;S?uP z(d}9lA6cmnLZ=_zREGfo$Xf|?_S=X!eLMhLC}%Y=fV!U%%ZCvlO197<)x&WFmJjv~ zOsLW9zB<0mi|`#x{V+zRb@a|JILd03>{U2q+T(?D+$Wzb=K{BW2TbC?e* z+DNm!?Sn)Dyu0{Bqq}L;2PyGLo?@+cZ$YR8qlGF00}&%HiZSuQqG-~LrPdbg z8;)W?6&D!(q_qY6Ht_FIy9jQgVEQ*g6=v(r#2=z*fve&oz{IEmoct1Eh21x|DB`_% zFbQyBWutNjtA4!5^o~X=d~AZzDoysqqyto*%yM_~Ci16?G6J^2LJ_H^E|B~C+rjHi;RD;q%~ zA+u8OQjB6T^;QgNCyS5!23oY-K_^Wj33+u@r)S8qcpQe-qv1oVF+!XHR%^e zmF<=i6)JSuVKkez8v01%;IOO0p(;ypqy*UEXW82ed!yo1dTgW_?oYsiui|}7{b0gu z&3Fo08~f4&+t_5P#N<{#RxfPi_&1Sksrd2YU}dXVwrcS$tB<1)%k5EV+Nxc&fy4Ic z%GTEU`;~gppvRCV6gnqL7R+L)v3^-wS-<*w1OBXC!9VY>{(f1jU;Qh6rGNgl{GZit zQ_jJmyx(P)*ceuCCedtMs_Ac)*^gt2u|xQsvS*P9Pz98^o$gGpr3$oggx4K!o16pO zX%8W?Yr}WAc9hDPl}*FzU*kOYXx`_U&)1^tQSda6$XK!rX6LkvoG*cG+#ZwY>>x2IxoMe#pJ z4rizmBQXd@j&;ZcQ(%5mB|eqbym7^gx)F1n`bZKmaZ4PhiZ}GfrGY=Z?)l#RHlkXk zQzNDOaz!9sHkL|`^l!D_k?{53rC;{9MuKv}@jAa>Pa?2Yc`u79Y89{eP{c|Kwyl;| z=M}>sE?Qj%$`#FL$yk)<2KTTU%+2_k$?YHHleYgC#m}+x%RY;s<|0N*Fx{c@9Zh6^m4ll zuQAUPIT{E}OzbP|j%O6fDYuny2`Ll`yY#kCd$Ur>***J6@tm|{Cp0d!f`LbAkImG) zpx`-E^02CM-4sTv`aREw1rr0CFuV5*_i+h423`6du66~UMwL_!e4}Ya_~f#vKrLP= z_l6Nt_)@()5^EH&Y5&VjK`V_9JMh#hO?VY7$n@CG`^vT-P1>-+sl*|LkA=7*K|pgv zaJA z6O2AKgK@7w-8W)Cv@v(@PNe` z#=qMQ!O%uizyV1QmdL1Aif6+Ty-GCFil>;gjJtJ$NRAg#ySR@km^tCOqBWBA&K^qS z6Yi=Yf87_lskffW^jJR8u(!wl#2?C<+k6%aK&0G*B?2P|Rx}O{Gc>yXpgSA*I^GFT zhB4^Z#7Gn?5mK0-%1($#g-tDf`;H1jwK;`h!j{B$p40-%@cErqn9Q&sF|j{O$ZL8U=3^MAiCL zw5$}7fk@zy`D!g=KC@s@0RuTi1-XDbMO4FsJh~fE{Um|*YOC+O;wsid!7wRO#6A9W zgxqJeC~I&>6CEZRmuxooO85#}P1va40oBzpA_?EH5(dLm~)K}VkvebSsA{&HLY*0xIfb; z!~W~}!7v&Kq^d@mX=|yU>fNpK({g9 zj#Bj9rStvv{zd2F@Oc09<3+1kTRPY|JoutdzLDw)Hu!s1Y`fwD+Q!tKJniX#abKk)K--8XYRu)(C(rnva`ZKO> zrz2jygh!x2fQ_b{6qK#y&wpNCX}0j%`e@=$9z2c!_KNOVg1$zB8ClzZ{xdrV$-v8! zx3K07kSs;aI6a+|2skJcP`=MvLTD0mZ1c|yM)f?P4wiUfi6I^33S}Ea4x?T$O*(|t z)uNa3&DFbB9bS&2I}pHv!K(06G^r9;sn%-MsJ*n;00CYH1AiO`y$gVePgYV&vzAIq z=_*-;9nLp+ZA;73n1>~u@KQSEeeY?UMdSEq}O+uL&76E4Sa*qaE43JkLeBfaPM z!MMS(!=XzkipnN6AN019kxlObs~`+NRD(n%wGXTmS=7a{n-7p@&EduFrHw=_eeCfa z2gjfz9MXyYZ16HKfuxbtDtd%riRC)4@Fg4fR?EOkx^$@S-v?dL_;#b=5c*y!;wfW~ zOB7`QIMG?(Nf4EgM63x$96>gvIbM1o(yjyJaSi&<;wy#{lnP?(d3U^zqu_oVbnzG< zO}~Ol^Zol!5}bYCmQ#Ee&mDrnE*P-t?GBk0QEKv{=k3jl-( z6!)c<{%O>o|MQ;={SN~z@c8Oi&i`9m+iaNUzt=Zk?|*rj4?9sZ3ww&rHp2XC&!n=6 zJIgsQrKG4ck5imOt90Cl&af2n*v3U>ig(8|^4L|Rm24Zx=m!Ahhx8@KYr+C;P(GPqz{jzyi{{fd2J~pl#c<9P5ejI%Ug>01g2gO}^N&8UhLooFXgmo~- z^yPzB!jk0-XfXVxA_$-!`sUzo!2kC7-veU$iYR7bxGzVNx_WF7*D$K>y2QsV? zu6`r=)~Kz`A;nqfHN2c-h@^er5JZ08%!d3Q!6eE9u?x7)1>}92Pk=^kt0B6*HNV?U z*=@C$F=_qLtnYWxRg1x^#r;7r1P!9Twt6-Rh9NvWfW;sf4Dt_8Lc|1Pg&hRD)r{FH zCQS+(8hEy&(L=@i_)`u(T$6lg@lF~MO`;61>87dO+i>LPkOM-nyqAI~Pb>c(#=Shy zH#k%wZF>3oa1+6!qUL!&R?4KLP_$s_0Fb{jYhnST%`V^lWI=H)HR&W~t)!>+s>`2a z4s*No_ug7c={5Oy=yyA#8HV592KObd9^nnFlVCjXy8+Pl9w}NZS7+m~7P}2qk%TM7 z>VJa;U*eE1LvPy)!D3{MU76AJwTf4-i*73WJIl=WEC4083s4=vS(_yAY%$X;L;s)n z@i^p1QJ-W2%!~hNZZ^#PPiyP1`u~f3zLjS-UnCJ;Wqph4<2Z^2qT~fVIfExlOS>Q2 z7pKRa-P4nU!yh{B-Sfk<3)+_prd@@bS$es6m2-1^TM&lHIWqY!Jx**B_PfMNcfwx9 zqv(lX8cbqc>c}FLW!4#GuC!3yzaRUf-gy*F`>`S?>;o*$y4+iZzl6`qjiT&`-d@r~ zmsLkfk<*K&`U(T)t*iu8ZDm6B66=e}pP~v>K-xwR82_I1%W%W(=5oVXP;xd4-wN0e zwzBP^fK*ydc4pbt>L8UT!F3c(Wn!Y?~cXS#F!vQH)A+Xs{zr zF30W`xZhE>T-F|&Eb-(q5kTy*vB}{T9k>-ee{vHi&S?chdMn3I_oy-~u>ixKz2{@8 zYcSRzHL?b-DZY{Nh*^D3y&)}MhZXtgn~W# zwugg7=;6LU7046zko5MlxC{I^@hPh8x8JRjQdMKExcw&Zu8vVVqRCc@&&bv6C04$y zU0w08ANpxVRyde&PH-Co?@4L%6$Z5SwFXor6I;+PCIJh%MHu=p&n488`ndHHeS`@d z^61kS?qB0Y#_)6$d4`TT@(?4n-Y<4t$F`3J{k6((BQxF{!e3LqiNcI>zkXe z`Tt(z!yIOlSU}GYp2&`Bhes1ppSF;4B% zi!(6$oGUoya+Ryb9NF~fRx_zPm0^k9RkoBJVWB+P{V_X4uabCL z83lB2tgL)JJn#pxl_KL}GE2q)OqbcvIzyB)1g!A48i5Hz_2~n^yYYvh{{?2;9^r9% zV+gqVWe$NX|E)%0DiMDAKqQ^{LyW~IKb-Pi2s|To!JS-r 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 Date: Thu, 30 Nov 2017 17:07:26 -0700 Subject: [PATCH 097/129] Send storage info only for specific mountpoints. --- myDevices/system/systeminfo.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index e60233d..63820be 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -79,10 +79,10 @@ def getDiskInfo(self): 'channel': 'sys:storage:/;usage', 'value': 6353821696 }, { - 'channel': 'sys:storage:/dev;capacity', + 'channel': 'sys:storage:/mnt/cdrom;capacity', 'value': 479383552 }, { - 'channel': 'sys:storage:/dev;usage', + 'channel': 'sys:storage:/mnt/cdrom;usage', 'value': 0 }] """ @@ -90,10 +90,12 @@ def getDiskInfo(self): try: for partition in psutil.disk_partitions(True): try: - usage = psutil.disk_usage(partition.mountpoint) - if usage.total: - cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used) - cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total) + mount_dir = partition.mountpoint.split('/')[1] + if partition.mountpoint == '/' or mount_dir in ('mnt', 'mount', 'Volumes'): + usage = psutil.disk_usage(partition.mountpoint) + if usage.total: + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used) + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total) except: pass except: From 644963da6e3dbbaf47aaf223654eef1c1f1581d7 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 5 Dec 2017 16:59:48 -0700 Subject: [PATCH 098/129] Send hardware ID when authenticating. --- myDevices/cloud/apiclient.py | 17 +++++++++++++++-- myDevices/system/hardware.py | 34 ++++++++++++++++++++------------- myDevices/test/hardware_test.py | 2 +- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index 84ad048..31b5a0c 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -2,6 +2,7 @@ from concurrent.futures import ThreadPoolExecutor import json from myDevices.utils.logger import error, exception +from myDevices.system.hardware import Hardware class CayenneApiClient: def __init__(self, host): @@ -36,14 +37,26 @@ def sendRequest(self, method, uri, body=None): return None return response exception("No data received") + + def getMessageBody(self, inviteCode): + body = {'id': inviteCode} + hardware = Hardware() + hardware_id = hardware.getMac() + if hardware_id: + body['type'] = 'mac' + body['hardware_id'] = hardware_id + elif hardware.Serial: + body['type'] = 'raspberrypi' + body['hardware_id'] = hardware.Serial + return json.dumps(body) def authenticate(self, inviteCode): - body = json.dumps({'id': inviteCode}) + body = self.getMessageBody(inviteCode) url = '/things/key/authenticate' return self.sendRequest('POST', url, body) def activate(self, inviteCode): - body = json.dumps({'id': inviteCode}) + body = self.getMessageBody(inviteCode) url = '/things/key/activate' return self.sendRequest('POST', url, body) diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index 5e8295a..c457284 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -4,7 +4,7 @@ """ import re import sys -from uuid import getnode +import netifaces from myDevices.utils.logger import exception, info, warn, error, debug BOARD_REVISION = 0 @@ -40,7 +40,8 @@ class Hardware: def __init__(self): """Initialize board revision and model info""" - self.Revision = "0" + self.Revision = '0' + self.Serial = None try: with open('/proc/cpuinfo','r') as f: for line in f: @@ -49,8 +50,10 @@ def __init__(self): continue key = splitLine[0].strip() value = splitLine[1].strip() - if key=='Revision': + if key == 'Revision': self.Revision = value + if key == 'Serial' and value != len(value) * '0': + self.Serial = value except: exception ("Error reading cpuinfo") self.model = 'Unknown' @@ -109,13 +112,18 @@ def getModel(self): """Return model name as string""" return self.model - def getMac(self, format=2): - """Return MAC address as string""" - if format < 2: - format = 2 - if format > 4: - format = 4 - mac_num = hex(getnode()).replace('0x', '').upper() - mac = '-'.join(mac_num[i : i + format] for i in range(0, 11, format)) - return mac - + def getMac(self): + """Return MAC address as a string or None if no MAC address is found""" + interfaces = ['eth0', 'wlan0'] + try: + interfaces.append(netifaces.gateways()['default'][netifaces.AF_INET][1]) + except: + pass + for interface in interfaces: + try: + return netifaces.ifaddresses(interface)[netifaces.AF_LINK][0]['addr'] + except ValueError: + pass + except: + exception('Error getting MAC address') + return None diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py index 7e1cd6c..6eb69d1 100644 --- a/myDevices/test/hardware_test.py +++ b/myDevices/test/hardware_test.py @@ -20,7 +20,7 @@ def testGetModel(self): def testGetMac(self): mac = self.hardware.getMac() info(mac) - self.assertRegex(mac, '^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$') + self.assertRegex(mac, '^([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})$') def testBoardRevision(self): info(BOARD_REVISION) From 3109c3245d2fe5a7081ec4eb53f923c4d0775870 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 5 Dec 2017 17:02:38 -0700 Subject: [PATCH 099/129] Remove unused attribute. --- myDevices/cloud/download_speed.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index 0e63d04..65c5264 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -1,7 +1,6 @@ """ This module provides a class for testing download speed """ -import netifaces from datetime import datetime, timedelta from os import path, remove from urllib import request, error @@ -26,7 +25,6 @@ def __init__(self, config): self.downloadSpeed = None self.testTime = None self.isRunning = False - self.interface = None self.Start() self.config = config #add a random delay to the start of download @@ -51,7 +49,6 @@ def Test(self): def TestDownload(self): """Test download speed by retrieving a file""" try: - self.interface = netifaces.gateways()['default'][netifaces.AF_INET][1] a = datetime.now() info('Executing regular download test for network speed') url = self.config.get('Agent', 'DownloadSpeedTestUrl', defaultUrl) From 879193ac261fffb4b8a11cc2532acd91e102e09e Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 5 Dec 2017 18:02:32 -0700 Subject: [PATCH 100/129] Process cmd.json topic. --- myDevices/cloud/cayennemqtt.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index ef7992c..7871122 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -7,6 +7,7 @@ # Topics DATA_TOPIC = 'data/json' COMMAND_TOPIC = 'cmd' +COMMAND_JSON_TOPIC = 'cmd.json' COMMAND_RESPONSE_TOPIC = 'response' # Data Channels @@ -127,6 +128,7 @@ def connect_callback(self, client, userdata, flags, rc): # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. client.subscribe(self.get_topic_string(COMMAND_TOPIC, True)) + client.subscribe(self.get_topic_string(COMMAND_JSON_TOPIC, False)) def disconnect_callback(self, client, userdata, rc): """The callback for when the client disconnects from the server. @@ -155,17 +157,18 @@ def message_callback(self, client, userdata, msg): """ try: message = {} - try: + if msg.topic[-len(COMMAND_JSON_TOPIC):] == COMMAND_JSON_TOPIC: message['payload'] = loads(msg.payload.decode()) message['cmdId'] = message['payload']['cmdId'] - except decoder.JSONDecodeError: + channel = message['payload']['channel'].split('/')[-1].split(';') + else: payload = msg.payload.decode().split(',') if len(payload) > 1: message['cmdId'] = payload[0] message['payload'] = payload[1] else: message['payload'] = payload[0] - channel = msg.topic.split('/')[-1].split(';') + channel = msg.topic.split('/')[-1].split(';') message['channel'] = channel[0] if len(channel) > 1: message['suffix'] = channel[1] @@ -173,7 +176,7 @@ def message_callback(self, client, userdata, msg): if self.on_message: self.on_message(message) except: - exception("Couldn't process: "+msg.topic+" "+str(msg.payload)) + exception('Error processing message: {} {}'.format(msg.topic, str(msg.payload))) def get_topic_string(self, topic, append_wildcard=False): """Return a topic string. From f60023f51d4df58a20e2f2d52702cee145f4f71a Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 8 Dec 2017 13:39:35 -0700 Subject: [PATCH 101/129] Consolidate device model checking. Update hardware ID info sent for Pi authentication. --- myDevices/cloud/apiclient.py | 15 ++++++------- myDevices/devices/bus.py | 6 +++--- myDevices/devices/digital/gpio.py | 6 +++--- myDevices/devices/i2c.py | 6 +++--- myDevices/devices/spi.py | 6 +++--- myDevices/system/hardware.py | 35 +++++++++++++++++++++---------- myDevices/system/systemconfig.py | 4 ++-- myDevices/test/hardware_test.py | 3 +++ setup.py | 2 +- 9 files changed, 50 insertions(+), 33 deletions(-) diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index 31b5a0c..59dbf24 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -41,13 +41,14 @@ def sendRequest(self, method, uri, body=None): def getMessageBody(self, inviteCode): body = {'id': inviteCode} hardware = Hardware() - hardware_id = hardware.getMac() - if hardware_id: - body['type'] = 'mac' - body['hardware_id'] = hardware_id - elif hardware.Serial: - body['type'] = 'raspberrypi' - body['hardware_id'] = hardware.Serial + if hardware.Serial and hardware.isRaspberryPi(): + body['type'] = 'rpi' + body['hardware_id'] = hardware.Serial + else: + hardware_id = hardware.getMac() + if hardware_id: + body['type'] = 'mac' + body['hardware_id'] = hardware_id return json.dumps(body) def authenticate(self, inviteCode): diff --git a/myDevices/devices/bus.py b/myDevices/devices/bus.py index b44c30f..348aba5 100644 --- a/myDevices/devices/bus.py +++ b/myDevices/devices/bus.py @@ -20,8 +20,8 @@ from myDevices.system.version import OS_VERSION, OS_JESSIE, OS_WHEEZY from myDevices.system.hardware import Hardware -MODEL = Hardware().getModel() -if MODEL == 'Tinker Board': +hardware = Hardware() +if hardware.isTinkerBoard(): # Tinker Board only supports I2C and SPI for now. These are enabled by default and # don't need to load any modules. BUSLIST = { @@ -33,7 +33,7 @@ "enabled": True, } } -elif 'BeagleBone' in MODEL: +elif hardware.isBeagleBone(): BUSLIST = { "I2C": { "enabled": True, diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index d99a5e5..1591901 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -363,10 +363,10 @@ def getFunctionString(self, channel): return function_string def setPinMapping(self): - model = Hardware().getModel() - if model == 'Tinker Board': + hardware = Hardware() + if hardware.isTinkerBoard(): self.MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] - elif 'BeagleBone' in model: + elif hardware.isBeagleBone(): self.MAPPING = {"headers": {"P9": ["GND", "GND", "V33", "V33", "V50", "V50", "V50", "V50", "PWR", "RST", 30, 60, 31, 50, 48, 51, 5, 4, "I2C2_SCL", "I2C2_SDA", 3, 2, 49, 15, 117, 14, 115, "SPI1_CS0", "SPI1_D0", 112, "SPI1_CLK", "VDD_ADC", "AIN4", "GNDA_ADC", "AIN6", "AIN5", "AIN2", "AIN3", "AIN0", "AIN1", 20, 7, "GND", "GND", "GND", "GND"], "P8": ["GND", "GND", "MMC1_DAT6", "MMC1_DAT7", "MMC1_DAT2", "MMC1_DAT3", 66, 67, 69, 68, 45, 44, 23, 26, 47, 46, 27, 65, 22, "MMC1_CMD", "MMC1_CLK", "MMC1_DAT5", "MMC1_DAT4", "MMC1_DAT1", "MMC1_DAT0", 61, "LCD_VSYNC", "LCD_PCLK", "LCD_HSYNC", "LCD_ACBIAS", "LCD_DATA14", "LCD_DATA15", "LCD_DATA13", "LCD_DATA11", "LCD_DATA12", "LCD_DATA10", "LCD_DATA8", "LCD_DATA9", "LCD_DATA6", "LCD_DATA7", "LCD_DATA4", "LCD_DATA5", "LCD_DATA2", "LCD_DATA3", "LCD_DATA0", "LCD_DATA1"]}, "order": ["P9", "P8"]} diff --git a/myDevices/devices/i2c.py b/myDevices/devices/i2c.py index 0321d13..c105c50 100644 --- a/myDevices/devices/i2c.py +++ b/myDevices/devices/i2c.py @@ -52,10 +52,10 @@ def __init__(self, slave): raise Exception("SLAVE_ADDRESS_USED") self.channel = 0 - model = Hardware().getModel() - if BOARD_REVISION > 1 or model == 'Tinker Board': + hardware = Hardware() + if BOARD_REVISION > 1 or hardware.isTinkerBoard(): self.channel = 1 - elif 'BeagleBone' in model: + elif hardware.isBeagleBone(): self.channel = 2 Bus.__init__(self, "I2C", "/dev/i2c-%d" % self.channel) diff --git a/myDevices/devices/spi.py b/myDevices/devices/spi.py index 66cf875..2cc5c78 100644 --- a/myDevices/devices/spi.py +++ b/myDevices/devices/spi.py @@ -87,10 +87,10 @@ def SPI_IOC_MESSAGE(count): class SPI(Bus): def __init__(self, chip=0, mode=0, bits=8, speed=0, init=True): bus = 0 - model = Hardware().getModel() - if model == 'Tinker Board': + hardware = Hardware() + if hardware.isTinkerBoard(): bus = 2 - elif 'BeagleBone' in model: + elif hardware.isBeagleBone(): bus = 1 Bus.__init__(self, "SPI", "/dev/spidev%d.%d" % (bus, chip)) self.chip = chip diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index c457284..b79dd20 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -4,7 +4,6 @@ """ import re import sys -import netifaces from myDevices.utils.logger import exception, info, warn, error, debug BOARD_REVISION = 0 @@ -58,25 +57,25 @@ def __init__(self): exception ("Error reading cpuinfo") self.model = 'Unknown' if self.Revision == 'Beta': - self.model = 'Model B (Beta)' + self.model = 'Raspberry Pi Model B (Beta)' if self.Revision in ('000d', '000e', '000f', '0002', '0003', '0004', '0005', '0006'): - self.model = 'Model B' + self.model = 'Raspberry Pi Model B' if self.Revision in ('0007', '0008', '0009'): - self.model = 'Model A' + self.model = 'Raspberry Pi Model A' if self.Revision in ('0010', '0013', '900032'): - self.model = 'Model B +' + self.model = 'Raspberry Pi Model B +' if self.Revision in ('0011', '0014'): - self.model = 'Compute Module' + self.model = 'Raspberry Pi Compute Module' if self.Revision in ('0012', '0015'): - self.model = 'Model A+' + self.model = 'Raspberry Pi Model A+' if self.Revision in ('a01041', 'a21041', 'a22042'): - self.model = 'Pi 2 Model B' + self.model = 'Raspberry Pi 2 Model B' if self.Revision in ('900092', '900093'): - self.model = 'Zero' + self.model = 'Raspberry Pi Zero' if self.Revision in ('9000c1',): - self.model = 'Zero W' + self.model = 'Raspberry Pi Zero W' if self.Revision in ('a02082', 'a22082'): - self.model = 'Pi 3 Model B' + self.model = 'Raspberry Pi 3 Model B' if 'Rockchip' in CPU_HARDWARE: self.model = 'Tinker Board' self.manufacturer = 'Element14/Premier Farnell' @@ -114,6 +113,8 @@ def getModel(self): def getMac(self): """Return MAC address as a string or None if no MAC address is found""" + # Import netifaces here to prevent error importing this module in setup.py + import netifaces interfaces = ['eth0', 'wlan0'] try: interfaces.append(netifaces.gateways()['default'][netifaces.AF_INET][1]) @@ -127,3 +128,15 @@ def getMac(self): except: exception('Error getting MAC address') return None + + def isRaspberryPi(self): + """Return True if device is a Raspberry Pi""" + return 'Raspberry Pi' in self.model + + def isTinkerBoard(self): + """Return True if device is a Tinker Board""" + return 'Tinker Board' == self.model + + def isBeagleBone(self): + """Return True if device is a BeagleBone""" + return 'BeagleBone' in self.model diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index 4ee690d..6399842 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -30,7 +30,7 @@ def ExecuteConfigCommand(config_id, parameters=''): config_id: Id of command to run parameters: Parameters to use when executing command """ - if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): + if not Hardware().isRaspberryPi(): return (1, 'Not supported') debug('SystemConfig::ExecuteConfigCommand') if config_id == 0: @@ -53,7 +53,7 @@ def RestartDevice(): def getConfig(): """Return dict containing configuration settings""" config = {} - if any(model in Hardware().getModel() for model in ('Tinker Board', 'BeagleBone')): + if not Hardware().isRaspberryPi(): return config commands = {10: 'DeviceTree', 18: 'Serial', 20: 'OneWire', 21: 'I2C', 22: 'SPI'} for command, name in commands.items(): diff --git a/myDevices/test/hardware_test.py b/myDevices/test/hardware_test.py index 6eb69d1..8e5e997 100644 --- a/myDevices/test/hardware_test.py +++ b/myDevices/test/hardware_test.py @@ -35,6 +35,9 @@ def testCpuHardware(self): info(CPU_HARDWARE) self.assertNotEqual(CPU_HARDWARE, '') + def testDeviceVerification(self): + device_checks = (self.hardware.isRaspberryPi(), self.hardware.isTinkerBoard(), self.hardware.isBeagleBone()) + self.assertEqual(device_checks.count(True), 1) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/setup.py b/setup.py index 215541a..68eceba 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ os.system('adduser {} i2c'.format(username)) relogin = True -if Hardware().getModel() == 'Tinker Board': +if Hardware().isTinkerBoard(): # Add spi group if it doesn't exist all_groups = [g.gr_name for g in grp.getgrall()] if not 'spi' in all_groups: From 61247317686e17ff7971357bc09f67368663d03a Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 18 Dec 2017 10:53:51 -0700 Subject: [PATCH 102/129] Call activate only once and save off the credentials. --- myDevices/cloud/apiclient.py | 6 +----- myDevices/cloud/client.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index 59dbf24..cd93b42 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -70,11 +70,7 @@ def getCredentials(self, content): return json.loads(body) def loginDevice(self, inviteCode): - response = self.authenticate(inviteCode) + response = self.activate(inviteCode) if response and response.status_code == 200: return self.getCredentials(response.content) - if not response or response.status_code == 412: - response = self.activate(inviteCode) - if response and response.status_code == 200: - return self.getCredentials(response.content) return None diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 60cb195..6203dcd 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -160,9 +160,9 @@ def __init__(self, host, port, cayenneApiHost): self.CayenneApiHost = cayenneApiHost self.config = Config(APP_SETTINGS) self.networkConfig = Config(NETWORK_SETTINGS) - self.username = None - self.password = None - self.clientId = None + self.username = self.config.get('Agent', 'Username', None) + self.password = self.config.get('Agent', 'Password', None) + self.clientId = self.config.get('Agent', 'ClientID', None) self.connected = False self.exiting = Event() @@ -181,7 +181,8 @@ def Start(self): if not self.installDate: self.installDate = int(time()) self.config.set('Agent', 'InstallDate', self.installDate) - self.CheckSubscription() + if not self.username and not self.password and not self.clientId: + self.CheckSubscription() if not self.Connect(): error('Error starting agent') return @@ -296,6 +297,9 @@ def CheckSubscription(self): self.username = credentials['mqtt']['username'] self.password = credentials['mqtt']['password'] self.clientId = credentials['mqtt']['clientId'] + self.config.set('Agent', 'Username', self.username) + self.config.set('Agent', 'Password', self.password) + self.config.set('Agent', 'ClientID', self.clientId) except: exception('Invalid credentials, closing the process') raise SystemExit From b733f95ed97297a1ecfdf9c6e53c43a87a279186 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 9 Jan 2018 17:07:56 -0700 Subject: [PATCH 103/129] Send system info in activation message. --- myDevices/cloud/apiclient.py | 18 +++++++++++++++++ myDevices/cloud/client.py | 2 -- myDevices/system/systeminfo.py | 33 ++++++++++++++++++++----------- myDevices/test/apiclient_test.py | 21 ++++++++++++++++++++ myDevices/test/systeminfo_test.py | 4 ++-- 5 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 myDevices/test/apiclient_test.py diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index cd93b42..9e69780 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -3,6 +3,9 @@ import json from myDevices.utils.logger import error, exception from myDevices.system.hardware import Hardware +from myDevices.system.systeminfo import SystemInfo +from myDevices.cloud import cayennemqtt +from myDevices.devices.digital.gpio import NativeGPIO class CayenneApiClient: def __init__(self, host): @@ -49,6 +52,21 @@ def getMessageBody(self, inviteCode): if hardware_id: body['type'] = 'mac' body['hardware_id'] = hardware_id + try: + system_data = [] + cayennemqtt.DataChannel.add(system_data, cayennemqtt.SYS_HARDWARE_MAKE, value=hardware.getManufacturer(), type='string', unit='utf8') + cayennemqtt.DataChannel.add(system_data, cayennemqtt.SYS_HARDWARE_MODEL, value=hardware.getModel(), type='string', unit='utf8') + system_info = SystemInfo() + capacity_data = system_info.getMemoryInfo((cayennemqtt.CAPACITY,)) + capacity_data += system_info.getDiskInfo((cayennemqtt.CAPACITY,)) + for item in capacity_data: + cayennemqtt.DataChannel.add(system_data, item['channel'], value=item['value'], type='memory', unit='byte') + body['properties'] = {} + body['properties']['gpiomap'] = NativeGPIO().MAPPING + if system_data: + body['properties']['sysinfo'] = system_data + except: + exception('Error getting system info') return json.dumps(body) def authenticate(self, inviteCode): diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 6203dcd..35a60b8 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -240,8 +240,6 @@ def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" try: data = [] - cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MAKE, value=self.hardware.getManufacturer()) - cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_HARDWARE_MODEL, value=self.hardware.getModel()) cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) cayennemqtt.DataChannel.add(data, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent', 'Version', __version__)) diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index 63820be..40ea0f8 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -17,8 +17,8 @@ def getSystemInformation(self): system_info = [] try: system_info += self.getCpuInfo() - system_info += self.getMemoryInfo() - system_info += self.getDiskInfo() + system_info += self.getMemoryInfo((cayennemqtt.USAGE,)) + system_info += self.getDiskInfo((cayennemqtt.USAGE,)) system_info += self.getNetworkInfo() except: exception('Error retrieving system info') @@ -45,8 +45,11 @@ def getCpuInfo(self): exception('Error getting CPU info') return cpu_info - def getMemoryInfo(self): - """Get disk usage information as a list formatted for Cayenne MQTT + def getMemoryInfo(self, types): + """Get memory information as a list formatted for Cayenne MQTT. + + Args: + types: Iterable containing types of memory info to retrieve matching cayennemqtt suffixes, e.g. cayennemqtt.USAGE Returned list example:: @@ -61,14 +64,19 @@ def getMemoryInfo(self): memory_info = [] try: vmem = psutil.virtual_memory() - cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.USAGE, value=vmem.total - vmem.available) - cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.CAPACITY, value=vmem.total) + if not types or cayennemqtt.USAGE in types: + cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.USAGE, value=vmem.total - vmem.available) + if not types or cayennemqtt.CAPACITY in types: + cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.CAPACITY, value=vmem.total) except: exception('Error getting memory info') return memory_info - def getDiskInfo(self): - """Get disk usage information as a list formatted for Cayenne MQTT + def getDiskInfo(self, types): + """Get disk information as a list formatted for Cayenne MQTT + + Args: + types: Iterable containing types of disk info to retrieve matching cayennemqtt suffixes, e.g. cayennemqtt.USAGE Returned list example:: @@ -90,12 +98,13 @@ def getDiskInfo(self): try: for partition in psutil.disk_partitions(True): try: - mount_dir = partition.mountpoint.split('/')[1] - if partition.mountpoint == '/' or mount_dir in ('mnt', 'mount', 'Volumes'): + if partition.mountpoint == '/': usage = psutil.disk_usage(partition.mountpoint) if usage.total: - cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used) - cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total) + if not types or cayennemqtt.USAGE in types: + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used) + if not types or cayennemqtt.CAPACITY in types: + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total) except: pass except: diff --git a/myDevices/test/apiclient_test.py b/myDevices/test/apiclient_test.py new file mode 100644 index 0000000..ad9bdce --- /dev/null +++ b/myDevices/test/apiclient_test.py @@ -0,0 +1,21 @@ +import unittest +from myDevices.utils.logger import exception, setDebug, info, debug, error, logToFile, setInfo +from myDevices.cloud.apiclient import CayenneApiClient +from json import loads + + +class ApiClientTest(unittest.TestCase): + def testMessageBody(self): + cayenneApiClient = CayenneApiClient('https://api.mydevices.com') + message = loads(cayenneApiClient.getMessageBody('invite_code')) + self.assertIn('id', message) + self.assertIn('type', message) + self.assertIn('hardware_id', message) + self.assertIn('properties', message) + self.assertIn('sysinfo', message['properties']) + self.assertIn('gpiomap', message['properties']) + + +if __name__ == '__main__': + setInfo() + unittest.main() diff --git a/myDevices/test/systeminfo_test.py b/myDevices/test/systeminfo_test.py index 52cd55f..93b892c 100644 --- a/myDevices/test/systeminfo_test.py +++ b/myDevices/test/systeminfo_test.py @@ -14,9 +14,9 @@ def testSystemInfo(self): self.assertIn('sys:cpu;load', self.info) self.assertIn('sys:cpu;temp', self.info) self.assertIn('sys:ram;usage', self.info) - self.assertIn('sys:ram;capacity', self.info) + # self.assertIn('sys:ram;capacity', self.info) self.assertIn('sys:storage:/;usage', self.info) - self.assertIn('sys:storage:/;capacity', self.info) + # self.assertIn('sys:storage:/;capacity', self.info) self.assertIn('sys:net;ip', self.info) # self.assertIn('sys:net;ssid', self.info) From d65ef7852201132881e359f994eadbd7ffc180c5 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 12 Jan 2018 11:42:53 -0700 Subject: [PATCH 104/129] Provide more detailed info in GPIO map. --- myDevices/cloud/apiclient.py | 2 +- myDevices/devices/digital/gpio.py | 279 ++++++++++++++++++++++++++++-- myDevices/test/apiclient_test.py | 2 +- myDevices/test/gpio_test.py | 5 +- 4 files changed, 268 insertions(+), 20 deletions(-) diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index 9e69780..6ec089c 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -62,7 +62,7 @@ def getMessageBody(self, inviteCode): for item in capacity_data: cayennemqtt.DataChannel.add(system_data, item['channel'], value=item['value'], type='memory', unit='byte') body['properties'] = {} - body['properties']['gpiomap'] = NativeGPIO().MAPPING + body['properties']['pinmap'] = NativeGPIO().MAPPING if system_data: body['properties']['sysinfo'] = system_data except: diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 1591901..e87a62f 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -19,7 +19,7 @@ from myDevices.utils.logger import debug, info, error, exception from myDevices.utils.singleton import Singleton from myDevices.devices.digital import GPIOPort -from myDevices.decorators.rest import request, response +from myDevices.decorators.rest import response from myDevices.system.hardware import BOARD_REVISION, Hardware from myDevices.utils.subprocess import executeCommand try: @@ -78,7 +78,7 @@ def __init__(self): error(err) def __del__(self): - if self.gpio_map: + if hasattr(self, 'gpio_map'): self.gpio_map.close() for value in self.valueFile.values(): if value: @@ -188,7 +188,7 @@ def __checkFilesystemFunction__(self, channel): if not valRet: return mode = 'w+' - if (gpio_library or 'BeagleBone' in Hardware().getModel()) and os.geteuid() != 0: + if (gpio_library or Hardware().isBeagleBone()) and os.geteuid() != 0: #On devices with root permissions on gpio files open the file in read mode from non-root process mode = 'r' for i in range(10): @@ -208,7 +208,7 @@ def __checkFilesystemValue__(self, channel): if not valRet: return mode = 'w+' - if (gpio_library or 'BeagleBone' in Hardware().getModel()) and os.geteuid() != 0: + if (gpio_library or Hardware().isBeagleBone()) and os.geteuid() != 0: #On devices with root permissions on gpio files open the file in read mode from non-root process mode = 'r' for i in range(10): @@ -353,11 +353,16 @@ def getFunction(self, channel): def getFunctionString(self, channel): f = self.getFunction(channel) function_string = 'UNKNOWN' - functions = {0:'IN', 1:'OUT', 2:'ALT5', 3:'ATL4', 4:'ALT0', 5:'ALT1', 6:'ALT2', 7:'ALT3', 8:'PWM', + functions = {0:'IN', 1:'OUT', 2:'ALT5', 3:'ALT4', 4:'ALT0', 5:'ALT1', 6:'ALT2', 7:'ALT3', 8:'PWM', 40:'SERIAL', 41:'SPI', 42:'I2C', 43:'PWM', 44:'GPIO', 45:'TS_XXXX', 46:'RESERVED', 47:'I2S'} if f >= 0: try: function_string = functions[f] + # On Raspberry Pis using the spi_bcm2835 driver SPI chip select is done via software rather than hardware + # so the pin function is OUT instead of ALT0. Here we override that if the SPI MOSI pin is set to ALT0 + # so the GPIO map in the UI will display the appropriate SPI pin info. + if channel in self.chip_select_pins and f == 1 and self.getFunction(self.spi_mosi_pin) == 4: + function_string = functions[4] except: pass return function_string @@ -365,21 +370,261 @@ def getFunctionString(self, channel): def setPinMapping(self): hardware = Hardware() if hardware.isTinkerBoard(): - self.MAPPING = ["V33", "V50", 252, "V50", 253, "GND", 17, 161, "GND", 160, 164, 184, 166, "GND", 167, 162, "V33", 163, 257, "GND", 256, 171, 254, 255, "GND", 251, "DNC", "DNC" , 165, "GND", 168, 239, 238, "GND", 185, 223, 224, 187, "GND", 188] + self.MAPPING = [{'name': 'GPIO', + 'map': [ + {'power': 'V33'}, + {'power': 'V50'}, + {'gpio': 252}, + {'power': 'V50'}, + {'gpio': 253}, + {'power': 'GND'}, + {'gpio': 17}, + {'gpio': 161}, + {'power': 'GND'}, + {'gpio': 160}, + {'gpio': 164}, + {'gpio': 184}, + {'gpio': 166}, + {'power': 'GND'}, + {'gpio': 167}, + {'gpio': 162}, + {'power': 'V33'}, + {'gpio': 163}, + {'gpio': 257}, + {'power': 'GND'}, + {'gpio': 256}, + {'gpio': 171}, + {'gpio': 254}, + {'gpio': 255}, + {'power': 'GND'}, + {'gpio': 251}, + {'dnc': True}, + {'dnc': True}, + {'gpio': 165}, + {'power': 'GND'}, + {'gpio': 168}, + {'gpio': 239}, + {'gpio': 238}, + {'power': 'GND'}, + {'gpio': 185}, + {'gpio': 223}, + {'gpio': 224}, + {'gpio': 187}, + {'power': 'GND'}, + {'gpio': 188} + ]}] elif hardware.isBeagleBone(): - self.MAPPING = {"headers": {"P9": ["GND", "GND", "V33", "V33", "V50", "V50", "V50", "V50", "PWR", "RST", 30, 60, 31, 50, 48, 51, 5, 4, "I2C2_SCL", "I2C2_SDA", 3, 2, 49, 15, 117, 14, 115, "SPI1_CS0", "SPI1_D0", 112, "SPI1_CLK", "VDD_ADC", "AIN4", "GNDA_ADC", "AIN6", "AIN5", "AIN2", "AIN3", "AIN0", "AIN1", 20, 7, "GND", "GND", "GND", "GND"], - "P8": ["GND", "GND", "MMC1_DAT6", "MMC1_DAT7", "MMC1_DAT2", "MMC1_DAT3", 66, 67, 69, 68, 45, 44, 23, 26, 47, 46, 27, 65, 22, "MMC1_CMD", "MMC1_CLK", "MMC1_DAT5", "MMC1_DAT4", "MMC1_DAT1", "MMC1_DAT0", 61, "LCD_VSYNC", "LCD_PCLK", "LCD_HSYNC", "LCD_ACBIAS", "LCD_DATA14", "LCD_DATA15", "LCD_DATA13", "LCD_DATA11", "LCD_DATA12", "LCD_DATA10", "LCD_DATA8", "LCD_DATA9", "LCD_DATA6", "LCD_DATA7", "LCD_DATA4", "LCD_DATA5", "LCD_DATA2", "LCD_DATA3", "LCD_DATA0", "LCD_DATA1"]}, - "order": ["P9", "P8"]} + self.MAPPING = [{'name': 'P9', + 'map': [ + {'power': 'GND'}, + {'power': 'GND'}, + {'power': 'V33'}, + {'power': 'V33'}, + {'power': 'V50'}, + {'power': 'V50'}, + {'power': 'V50'}, + {'power': 'V50'}, + {'power': 'PWR'}, + {'power': 'RST'}, + {'gpio': 30}, + {'gpio': 60}, + {'gpio': 31}, + {'gpio': 50}, + {'gpio': 48}, + {'gpio': 51}, + {'gpio': 5}, + {'gpio': 4}, + {'alt0': {'channel': 'sys:i2c:2', 'name': 'SCL'}}, + {'alt0': {'channel': 'sys:i2c:2', 'name': 'SDA'}}, + {'gpio': 3}, + {'gpio': 2}, + {'gpio': 49}, + {'gpio': 15}, + {'gpio': 117}, + {'gpio': 14}, + {'gpio': 115}, + {'gpio': 113, 'alt0': {'channel': 'sys:spi:1', 'name': 'CS0'}}, + {'gpio': 111, 'alt0': {'channel': 'sys:spi:1', 'name': 'D0'}}, + {'gpio': 112, 'alt0': {'channel': 'sys:spi:1', 'name': 'D1'}}, + {'gpio': 110, 'alt0': {'channel': 'sys:spi:1', 'name': 'SCLK'}}, + {'power': 'VDD_ADC'}, + {'analog': 4}, + {'power': 'GNDA_ADC'}, + {'analog': 6}, + {'analog': 5}, + {'analog': 2}, + {'analog': 3}, + {'analog': 0}, + {'analog': 1}, + {'gpio': 20}, + {'gpio': 7}, + {'power': 'GND'}, + {'power': 'GND'}, + {'power': 'GND'}, + {'power': 'GND'}]}, + {'name': 'P8', + 'map': [ + {'power': 'GND'}, + {'power': 'GND'}, + {'gpio': 38}, + {'gpio': 39}, + {'gpio': 34}, + {'gpio': 35}, + {'gpio': 66}, + {'gpio': 67}, + {'gpio': 69}, + {'gpio': 68}, + {'gpio': 45}, + {'gpio': 44}, + {'gpio': 23}, + {'gpio': 26}, + {'gpio': 47}, + {'gpio': 46}, + {'gpio': 27}, + {'gpio': 65}, + {'gpio': 22}, + {'gpio': 63}, + {'gpio': 62}, + {'gpio': 37}, + {'gpio': 36}, + {'gpio': 33}, + {'gpio': 32}, + {'gpio': 61}, + {'gpio': 86}, + {'gpio': 88}, + {'gpio': 87}, + {'gpio': 89}, + {'gpio': 10}, + {'gpio': 11}, + {'gpio': 9}, + {'gpio': 81}, + {'gpio': 8}, + {'gpio': 80}, + {'gpio': 78}, + {'gpio': 79}, + {'gpio': 76}, + {'gpio': 77}, + {'gpio': 74}, + {'gpio': 75}, + {'gpio': 72}, + {'gpio': 73}, + {'gpio': 70}, + {'gpio': 71} + ]}] else: - if BOARD_REVISION == 1: - self.MAPPING = ["V33", "V50", 0, "V50", 1, "GND", "1-WIRE", 14, "GND", 15, 17, 18, 21, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + if BOARD_REVISION == 1: + self.MAPPING = [{'name': 'P1', + 'map': [ + {'power': 'V33'}, + {'power': 'V50'}, + {'gpio': 0, 'alt0': {'channel': 'sys:i2c', 'name': 'SDA'}}, + {'power': 'V50'}, + {'gpio': 1, 'alt0': {'channel': 'sys:i2c', 'name': 'SCL'}}, + {'power': 'GND'}, + {'gpio': 4, 'alt0': {'channel': 'sys:clk', 'name': 'GPCLK'}}, + {'gpio': 14, 'alt0': {'channel': 'sys:uart', 'name': 'TX'}}, + {'power': 'GND'}, + {'gpio': 15, 'alt0': {'channel': 'sys:uart', 'name': 'RX'}}, + {'gpio': 17}, + {'gpio': 18}, + {'gpio': 21}, + {'power': 'GND'}, + {'gpio': 22}, + {'gpio': 23}, + {'power': 'V33'}, + {'gpio': 24}, + {'gpio': 10, 'alt0': {'channel': 'sys:spi', 'name': 'MOSI'}}, + {'power': 'GND'}, + {'gpio': 9, 'alt0': {'channel': 'sys:spi', 'name': 'MISO'}}, + {'gpio': 25}, + {'gpio': 11, 'alt0': {'channel': 'sys:spi', 'name': 'SCLK'}}, + {'gpio': 8, 'alt0': {'channel': 'sys:spi', 'name': 'CE0'}}, + {'power': 'GND'}, + {'gpio': 7, 'alt0': {'channel': 'sys:spi', 'name': 'CE1'}} + ]}] elif BOARD_REVISION == 2: - self.MAPPING = ["V33", "V50", 2, "V50", 3, "GND", "1-WIRE", 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7] + self.MAPPING = [{'name': 'P1', + 'map': [ + {'power': 'V33'}, + {'power': 'V50'}, + {'gpio': 2, 'alt0': {'channel': 'sys:i2c', 'name': 'SDA'}}, + {'power': 'V50'}, + {'gpio': 3, 'alt0': {'channel': 'sys:i2c', 'name': 'SCL'}}, + {'power': 'GND'}, + {'gpio': 4, 'alt0': {'channel': 'sys:clk', 'name': 'GPCLK'}}, + {'gpio': 14, 'alt0': {'channel': 'sys:uart', 'name': 'TX'}}, + {'power': 'GND'}, + {'gpio': 15, 'alt0': {'channel': 'sys:uart', 'name': 'RX'}}, + {'gpio': 17}, + {'gpio': 18}, + {'gpio': 27}, + {'power': 'GND'}, + {'gpio': 22}, + {'gpio': 23}, + {'power': 'V33'}, + {'gpio': 24}, + {'gpio': 10, 'alt0': {'channel': 'sys:spi', 'name': 'MOSI'}}, + {'power': 'GND'}, + {'gpio': 9, 'alt0': {'channel': 'sys:spi', 'name': 'MISO'}}, + {'gpio': 25}, + {'gpio': 11, 'alt0': {'channel': 'sys:spi', 'name': 'SCLK'}}, + {'gpio': 8, 'alt0': {'channel': 'sys:spi', 'name': 'CE0'}}, + {'power': 'GND'}, + {'gpio': 7, 'alt0': {'channel': 'sys:spi', 'name': 'CE1'}} + ]}] elif BOARD_REVISION == 3: - self.MAPPING = ["V33", "V50", 2, "V50", 3, "GND", "1-WIRE", 14, "GND", 15, 17, 18, 27, "GND", 22, 23, "V33", 24, 10, "GND", 9, 25, 11, 8, "GND", 7, "DNC", "DNC" , 5, "GND", 6, 12, 13, "GND", 19, 16, 26, 20, "GND", 21] + self.MAPPING = [{'name': 'P1', + 'map': [ + {'power': 'V33'}, + {'power': 'V50'}, + {'gpio': 2, 'alt0': {'channel': 'sys:i2c', 'name': 'SDA'}}, + {'power': 'V50'}, + {'gpio': 3, 'alt0': {'channel': 'sys:i2c', 'name': 'SCL'}}, + {'power': 'GND'}, + {'gpio': 4, 'overlay': {'channel': 'sys:1wire', 'name': 'DATA'}}, + {'gpio': 14, 'alt0': {'channel': 'sys:uart', 'name': 'TX'}}, + {'power': 'GND'}, + {'gpio': 15, 'alt0': {'channel': 'sys:uart', 'name': 'RX'}}, + {'gpio': 17}, + {'gpio': 18}, + {'gpio': 27}, + {'power': 'GND'}, + {'gpio': 22}, + {'gpio': 23}, + {'power': 'V33'}, + {'gpio': 24}, + {'gpio': 10, 'alt0': {'channel': 'sys:spi', 'name': 'MOSI'}}, + {'power': 'GND'}, + {'gpio': 9, 'alt0': {'channel': 'sys:spi', 'name': 'MISO'}}, + {'gpio': 25}, + {'gpio': 11, 'alt0': {'channel': 'sys:spi', 'name': 'SCLK'}}, + {'gpio': 8, 'alt0': {'channel': 'sys:spi', 'name': 'CE0'}}, + {'power': 'GND'}, + {'gpio': 7, 'alt0': {'channel': 'sys:spi', 'name': 'CE1'}}, + {'dnc': True}, + {'dnc': True}, + {'gpio': 5}, + {'power': 'GND'}, + {'gpio': 6}, + {'gpio': 12}, + {'gpio': 13}, + {'power': 'GND'}, + {'gpio': 19}, + {'gpio': 16}, + {'gpio': 26}, + {'gpio': 20}, + {'power': 'GND'}, + {'gpio': 21} + ]}] if isinstance(self.MAPPING, list): - self.pins = [pin for pin in self.MAPPING if type(pin) is int] - elif 'headers' in self.MAPPING: self.pins = [] - for header in self.MAPPING['headers'].values(): - self.pins.extend([pin for pin in header if type(pin) is int]) + for header in self.MAPPING: + self.pins.extend([pin['gpio'] for pin in header['map'] if 'gpio' in pin]) + try: + if Hardware().isRaspberryPi(): + self.chip_select_pins = [] + self.spi_mosi_pin = 10 + for header in self.MAPPING: + self.chip_select_pins.extend([pin['gpio'] for pin in header['map'] if 'alt0' in pin and pin['alt0']['name'] in ('CE0', 'CE1')]) + except: + pass diff --git a/myDevices/test/apiclient_test.py b/myDevices/test/apiclient_test.py index ad9bdce..cc95080 100644 --- a/myDevices/test/apiclient_test.py +++ b/myDevices/test/apiclient_test.py @@ -13,7 +13,7 @@ def testMessageBody(self): self.assertIn('hardware_id', message) self.assertIn('properties', message) self.assertIn('sysinfo', message['properties']) - self.assertIn('gpiomap', message['properties']) + self.assertIn('pinmap', message['properties']) if __name__ == '__main__': diff --git a/myDevices/test/gpio_test.py b/myDevices/test/gpio_test.py index efb0d4e..7d88fde 100644 --- a/myDevices/test/gpio_test.py +++ b/myDevices/test/gpio_test.py @@ -7,7 +7,10 @@ def setUp(self): self.gpio = NativeGPIO() def testGPIO(self): - for pin in self.gpio.pins: + pins = [] + for header in self.gpio.MAPPING: + pins.extend([pin['gpio'] for pin in header['map'] if 'gpio' in pin and 'alt0' not in pin]) + for pin in pins: info('Testing pin {}'.format(pin)) function = self.gpio.setFunctionString(pin, "OUT") if function == "UNKNOWN": From 680e9815cd9c21a27aa916c30081826b20c19fde Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 12 Jan 2018 12:40:29 -0700 Subject: [PATCH 105/129] Convert digital values to ints. --- myDevices/sensors/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index d77aa2c..62bde51 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -329,7 +329,7 @@ def GpioCommand(self, command, channel, value): elif value.lower() in ('out', 'output'): return str(self.gpio.setFunctionString(channel, 'out')) elif command in ('value', ''): - return self.gpio.digitalWrite(channel, value) + return self.gpio.digitalWrite(channel, int(value)) debug('GPIO command failed') return 'failure' From 1aa4a6f9dc2c97128cb5dc78e325652711d4476b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 12 Jan 2018 17:02:37 -0700 Subject: [PATCH 106/129] Return consistent values from config script. --- myDevices/test/systemconfig_test.py | 3 +- scripts/config.sh | 46 ++++++++++++++++++----------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/myDevices/test/systemconfig_test.py b/myDevices/test/systemconfig_test.py index 3084981..ef0c9ba 100644 --- a/myDevices/test/systemconfig_test.py +++ b/myDevices/test/systemconfig_test.py @@ -6,8 +6,9 @@ class SystemConfigTest(unittest.TestCase): def testSystemConfig(self): config = SystemConfig.getConfig() + info(config) if config: - for item in ('DeviceTree', 'Serial', 'I2C', 'SPI'): + for item in ('DeviceTree', 'Serial', 'I2C', 'SPI', 'OneWire'): self.assertIn(item, config) diff --git a/scripts/config.sh b/scripts/config.sh index 13652ac..ea0a011 100644 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -254,6 +254,7 @@ do_change_hostname() { sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\t$NEW_HOSTNAME/g" /etc/hosts ASK_TO_REBOOT=1 } + # $1 is 0 to enable overscan, 1 to disable it set_overscan() { # Stop if /boot is not a mountpoint @@ -270,6 +271,7 @@ set_overscan() { set_config_var disable_overscan 0 $CONFIG fi } + get_memory_split(){ if [ -e /boot/start_cd.elf ]; then # New-style memory split setting @@ -285,6 +287,7 @@ get_memory_split(){ echo "${MEMSPLIT_DESCRIPTION}" fi } + do_overscan() { RET=${args[1]} if [ $RET -eq 0 ] || [ $RET -eq 1 ]; then @@ -294,6 +297,7 @@ do_overscan() { return 1 fi } + do_ssh() { if [ -e /var/log/regen_ssh_keys.log ] && ! grep -q "^finished" /var/log/regen_ssh_keys.log; then echo "Initial ssh key generation still running. Please wait and try again." @@ -309,6 +313,7 @@ do_ssh() { return $RET fi } + do_devicetree() { CURRENT_SETTING="enabled" # assume not disabled DEFAULT= @@ -340,12 +345,14 @@ do_devicetree() { fi } + get_devicetree() { - CURRENT_SETTING="1" # assume not disabled - if [ -e $CONFIG ] && grep -q "^device_tree=$" $CONFIG; then - CURRENT_SETTING="0" - fi - echo "${CURRENT_SETTING}" + echo 0 # assume enabled since recent kernels don't appear to allow disabling the device tree anymore + # CURRENT_SETTING="1" # assume not disabled + # if [ -e $CONFIG ] && grep -q "^device_tree=$" $CONFIG; then + # CURRENT_SETTING="0" + # fi + # echo "${CURRENT_SETTING}" } #arg[1] enable I2C 1/0 arg[2] load by default 1/0 @@ -436,7 +443,6 @@ get_i2c() { fi } - #arg[1] enable SPI 1/0 arg[2] load by default 1/0 #again 0 enable 1 disable do_spi() { @@ -556,22 +562,26 @@ get_camera() { } get_serial() { - if ! grep -q "^T.*:.*:respawn:.*ttyAMA0" /etc/inittab; then - echo 0 - return 0 - fi + if grep -q -E "^enable_uart=1" $CONFIG ; then + echo 0 + elif grep -q -E "^enable_uart=0" $CONFIG ; then echo 1 - return 0 + elif [ -e /dev/serial0 ] ; then + echo 0 + else + echo 1 + fi } + get_w1(){ - output=$( cat $CONFIG | grep ' *dtoverlay*=*w1-gpio' ) - if [ -z "$output" ]; then - echo 0 - else - echo 1 - fi - return 0 + if grep -q -E "^dtoverlay=w1-gpio" $CONFIG; then + echo 0 + else + echo 1 + fi + return 0 } + do_w1(){ RET=${args[1]} CURRENT=$(get_w1) From 87ca546fbb9cf3d6bc340058926cc57cb00d289b Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 12 Jan 2018 17:04:43 -0700 Subject: [PATCH 107/129] Add overlay function for pins used by a device tree overlay. --- myDevices/devices/digital/gpio.py | 25 ++++++++++++++++++------- myDevices/test/gpio_test.py | 21 +++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index e87a62f..1ede7ee 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -21,6 +21,7 @@ from myDevices.devices.digital import GPIOPort from myDevices.decorators.rest import response from myDevices.system.hardware import BOARD_REVISION, Hardware +from myDevices.system.systemconfig import SystemConfig from myDevices.utils.subprocess import executeCommand try: import ASUS.GPIO as gpio_library @@ -221,9 +222,8 @@ def __checkFilesystemValue__(self, channel): sleep(0.01) def __digitalRead__(self, channel): - self.__checkFilesystemValue__(channel) - #self.checkDigitalChannelExported(channel) try: + self.__checkFilesystemValue__(channel) value = self.valueFile[channel].read(1) self.valueFile[channel].seek(0) if value[0] == '1': @@ -235,8 +235,6 @@ def __digitalRead__(self, channel): def __digitalWrite__(self, channel, value): self.__checkFilesystemValue__(channel) - #self.checkDigitalChannelExported(channel) - #self.checkPostingValueAllowed() try: if value == 1: value = '1' @@ -339,7 +337,7 @@ def wildcard(self, compact=False): f = "function" v = "value" values = {} - for i in self.pins: + for i in self.pins + self.overlay_pins: if compact: func = self.getFunction(i) else: @@ -358,6 +356,9 @@ def getFunctionString(self, channel): if f >= 0: try: function_string = functions[f] + except: + pass + try: # On Raspberry Pis using the spi_bcm2835 driver SPI chip select is done via software rather than hardware # so the pin function is OUT instead of ALT0. Here we override that if the SPI MOSI pin is set to ALT0 # so the GPIO map in the UI will display the appropriate SPI pin info. @@ -365,6 +366,12 @@ def getFunctionString(self, channel): function_string = functions[4] except: pass + try: + # If 1-Wire is enabled specify the pin function as a device tree overlay. + if channel in self.overlay_pins: + function_string = 'OVERLAY' + except: + pass return function_string def setPinMapping(self): @@ -521,7 +528,7 @@ def setPinMapping(self): {'power': 'V50'}, {'gpio': 1, 'alt0': {'channel': 'sys:i2c', 'name': 'SCL'}}, {'power': 'GND'}, - {'gpio': 4, 'alt0': {'channel': 'sys:clk', 'name': 'GPCLK'}}, + {'gpio': 4, 'overlay': {'channel': 'sys:1wire', 'name': 'DATA'}}, {'gpio': 14, 'alt0': {'channel': 'sys:uart', 'name': 'TX'}}, {'power': 'GND'}, {'gpio': 15, 'alt0': {'channel': 'sys:uart', 'name': 'RX'}}, @@ -551,7 +558,7 @@ def setPinMapping(self): {'power': 'V50'}, {'gpio': 3, 'alt0': {'channel': 'sys:i2c', 'name': 'SCL'}}, {'power': 'GND'}, - {'gpio': 4, 'alt0': {'channel': 'sys:clk', 'name': 'GPCLK'}}, + {'gpio': 4, 'overlay': {'channel': 'sys:1wire', 'name': 'DATA'}}, {'gpio': 14, 'alt0': {'channel': 'sys:uart', 'name': 'TX'}}, {'power': 'GND'}, {'gpio': 15, 'alt0': {'channel': 'sys:uart', 'name': 'RX'}}, @@ -618,10 +625,14 @@ def setPinMapping(self): ]}] if isinstance(self.MAPPING, list): self.pins = [] + self.overlay_pins = [] for header in self.MAPPING: self.pins.extend([pin['gpio'] for pin in header['map'] if 'gpio' in pin]) try: if Hardware().isRaspberryPi(): + if SystemConfig.getConfig()['OneWire'] == 1: + self.overlay_pins.extend([pin['gpio'] for pin in header['map'] if 'overlay' in pin and pin['overlay']['channel'] == 'sys:1wire']) + self.pins = [pin for pin in self.pins if pin not in self.overlay_pins] self.chip_select_pins = [] self.spi_mosi_pin = 10 for header in self.MAPPING: diff --git a/myDevices/test/gpio_test.py b/myDevices/test/gpio_test.py index 7d88fde..6e69df1 100644 --- a/myDevices/test/gpio_test.py +++ b/myDevices/test/gpio_test.py @@ -9,14 +9,14 @@ def setUp(self): def testGPIO(self): pins = [] for header in self.gpio.MAPPING: - pins.extend([pin['gpio'] for pin in header['map'] if 'gpio' in pin and 'alt0' not in pin]) + pins.extend([pin['gpio'] for pin in header['map'] if 'gpio' in pin and 'alt0' not in pin and 'overlay' not in pin]) for pin in pins: info('Testing pin {}'.format(pin)) - function = self.gpio.setFunctionString(pin, "OUT") - if function == "UNKNOWN": + function = self.gpio.setFunctionString(pin, 'OUT') + if function == 'UNKNOWN': info('Pin {} function UNKNOWN, skipping'.format(pin)) continue - self.assertEqual("OUT", function) + self.assertEqual('OUT', function) value = self.gpio.digitalWrite(pin, 1) self.assertEqual(value, 1) value = self.gpio.digitalWrite(pin, 0) @@ -24,12 +24,13 @@ def testGPIO(self): def testPinStatus(self): pin_status = self.gpio.wildcard() - # print(pin_status) - self.assertEqual(set(self.gpio.pins), set(pin_status.keys())) - for pin in pin_status.values(): - self.assertCountEqual(pin.keys(), ('function', 'value')) - self.assertGreaterEqual(pin['value'], 0) - self.assertLessEqual(pin['value'], 1) + info(pin_status) + self.assertEqual(set(self.gpio.pins + self.gpio.overlay_pins), set(pin_status.keys())) + for key, value in pin_status.items(): + self.assertCountEqual(value.keys(), ('function', 'value')) + if key in self.gpio.pins: + self.assertGreaterEqual(value['value'], 0) + self.assertLessEqual(value['value'], 1) if __name__ == '__main__': From e5bd3ac36799ab0f463e4c0f6f8c2a3ac8891da9 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 15 Jan 2018 14:06:39 -0700 Subject: [PATCH 108/129] Process new reset & halt messages. --- myDevices/cloud/cayennemqtt.py | 3 ++- myDevices/cloud/client.py | 32 +++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 7871122..f7cf8fd 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -24,7 +24,8 @@ SYS_UART = 'sys:uart' SYS_DEVICETREE = 'sys:devicetree' SYS_GPIO = 'sys:gpio' -SYS_POWER = 'sys:pwr' +SYS_POWER_RESET = 'sys:pwr:reset' +SYS_POWER_HALT = 'sys:pwr:halt' AGENT_VERSION = 'agent:version' AGENT_DEVICES = 'agent:devices' AGENT_MANAGE = 'agent:manage' diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 35a60b8..82fc975 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -117,6 +117,7 @@ def run(self): message = dumps(message) self.cloudClient.mqttClient.publish_packet(topic, message) message = None + self.cloudClient.writeQueue.task_done() except: exception("WriterThread Unexpected error") return @@ -243,6 +244,8 @@ def SendSystemInfo(self): cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) cayennemqtt.DataChannel.add(data, cayennemqtt.AGENT_VERSION, value=self.config.get('Agent', 'Version', __version__)) + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_POWER_RESET, value=0) + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_POWER_HALT, value=0) config = SystemConfig.getConfig() if config: channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} @@ -374,7 +377,7 @@ def ExecuteMessage(self, message): return channel = message['channel'] info('ExecuteMessage: {}'.format(message)) - if channel == cayennemqtt.SYS_POWER: + if channel in (cayennemqtt.SYS_POWER_RESET, cayennemqtt.SYS_POWER_HALT): self.ProcessPowerCommand(message) elif channel.startswith(cayennemqtt.DEV_SENSOR): self.ProcessSensorCommand(message) @@ -391,16 +394,27 @@ def ExecuteMessage(self, message): def ProcessPowerCommand(self, message): """Process command to reboot/shutdown the system""" - error = None + error_message = None try: - commands = {'reset': 'sudo shutdown -r now', 'halt': 'sudo shutdown -h now'} - output, result = executeCommand(commands[message['payload']]) - debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output)) - if result != 0: - error = 'Error executing shutdown command' + self.EnqueueCommandResponse(message, error_message) + commands = {cayennemqtt.SYS_POWER_RESET: 'sudo shutdown -r now', cayennemqtt.SYS_POWER_HALT: 'sudo shutdown -h now'} + if int(message['payload']) == 1: + debug('Processing power command') + data = [] + cayennemqtt.DataChannel.add(data, message['channel'], value=1) + self.EnqueuePacket(data) + self.writeQueue.join() + output, result = executeCommand(commands[message['channel']]) + debug('ProcessPowerCommand: {}, result: {}, output: {}'.format(message, result, output)) + if result != 0: + error_message = 'Error executing shutdown command' except Exception as ex: - error = '{}: {}'.format(type(ex).__name__, ex) - self.EnqueueCommandResponse(message, error) + error_message = '{}: {}'.format(type(ex).__name__, ex) + if error_message: + error(error_message) + data = [] + cayennemqtt.DataChannel.add(data, message['channel'], value=0) + self.EnqueuePacket(data) def ProcessAgentCommand(self, message): """Process command to manage the agent state""" From b6ba71e36787031c4523e39c39951eeb2d1598cd Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 15 Jan 2018 16:33:00 -0700 Subject: [PATCH 109/129] Use new update config file location for new agent. --- myDevices/cloud/updater.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/myDevices/cloud/updater.py b/myDevices/cloud/updater.py index d826ba5..fcb28b3 100644 --- a/myDevices/cloud/updater.py +++ b/myDevices/cloud/updater.py @@ -13,12 +13,12 @@ SETUP_NAME = 'myDevicesSetup_raspberrypi.sh' INSTALL_PATH = '/etc/myDevices/' UPDATE_PATH = INSTALL_PATH + 'updates/' -UPDATE_CFG = UPDATE_PATH + 'update' +UPDATE_CFG = UPDATE_PATH + 'updatecfg' SETUP_PATH = UPDATE_PATH + SETUP_NAME TIME_TO_CHECK = 60 + random.randint(60, 300) #seconds - at least 2 minutes TIME_TO_SLEEP = 60 -UPDATE_URL = 'https://updates.mydevices.com/raspberry/update' +UPDATE_URL = 'https://updates.mydevices.com/raspberry/updatecfg' SETUP_URL = 'https://updates.mydevices.com/raspberry/' try: From e4a76a426057ba463436661cfe5927ae33dfa726 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 22 Jan 2018 13:04:09 -0700 Subject: [PATCH 110/129] Fix config settings issues with device tree, 1-wire and serial. --- myDevices/cloud/cayennemqtt.py | 1 + myDevices/cloud/client.py | 7 +- scripts/config.sh | 154 ++++++++++++++++++++------------- 3 files changed, 101 insertions(+), 61 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index f7cf8fd..3cb635f 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -22,6 +22,7 @@ SYS_I2C = 'sys:i2c' SYS_SPI = 'sys:spi' SYS_UART = 'sys:uart' +SYS_ONEWIRE = 'sys:1wire' SYS_DEVICETREE = 'sys:devicetree' SYS_GPIO = 'sys:gpio' SYS_POWER_RESET = 'sys:pwr:reset' diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 82fc975..3341744 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -248,7 +248,8 @@ def SendSystemInfo(self): cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_POWER_HALT, value=0) config = SystemConfig.getConfig() if config: - channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} + channel_map = {'I2C': cayennemqtt.SYS_I2C, 'SPI': cayennemqtt.SYS_SPI, 'Serial': cayennemqtt.SYS_UART, + 'OneWire': cayennemqtt.SYS_ONEWIRE, 'DeviceTree': cayennemqtt.SYS_DEVICETREE} for key, channel in channel_map.items(): try: cayennemqtt.DataChannel.add(data, channel, value=config[key]) @@ -385,7 +386,7 @@ def ExecuteMessage(self, message): self.ProcessGpioCommand(message) elif channel == cayennemqtt.AGENT_DEVICES: self.ProcessDeviceCommand(message) - elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_DEVICETREE): + elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_ONEWIRE): self.ProcessConfigCommand(message) elif channel == cayennemqtt.AGENT_MANAGE: self.ProcessAgentCommand(message) @@ -442,7 +443,7 @@ def ProcessConfigCommand(self, message): error = None try: value = 1 - int(message['payload']) #Invert the value since the config script uses 0 for enable and 1 for disable - command_id = {cayennemqtt.SYS_I2C: 11, cayennemqtt.SYS_SPI: 12, cayennemqtt.SYS_UART: 13, cayennemqtt.SYS_DEVICETREE: 9} + command_id = {cayennemqtt.SYS_I2C: 11, cayennemqtt.SYS_SPI: 12, cayennemqtt.SYS_UART: 13, cayennemqtt.SYS_ONEWIRE: 19} result, output = SystemConfig.ExecuteConfigCommand(command_id[message['channel']], value) debug('ProcessConfigCommand: {}, result: {}, output: {}'.format(message, result, output)) if result != 0: diff --git a/scripts/config.sh b/scripts/config.sh index ea0a011..b7370f3 100644 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -315,35 +315,35 @@ do_ssh() { } do_devicetree() { - CURRENT_SETTING="enabled" # assume not disabled - DEFAULT= - if [ -e $CONFIG ] && grep -q "^device_tree=$" $CONFIG; then - CURRENT_SETTING="disabled" - DEFAULT=--defaultno - fi - RET=${args[1]} - if [ $RET -eq 0 ]; then - sed $CONFIG -i -e "s/^\(device_tree=\)$/#\1/" - sed $CONFIG -i -e "s/^#\(device_tree=.\)/\1/" - SETTING=enabled - elif [ $RET -eq 1 ]; then - sed $CONFIG -i -e "s/^#\(device_tree=\)$/\1/" - sed $CONFIG -i -e "s/^\(device_tree=.\)/#\1/" - if ! grep -q "^device_tree=$" $CONFIG; then - printf "device_tree=\n" >> $CONFIG - fi - SETTING=disabled - else - return 0 - fi - TENSE=is - REBOOT= - if [ $SETTING != $CURRENT_SETTING ]; then - TENSE="will be" - REBOOT=" after a reboot" - ASK_TO_REBOOT=1 - fi - + return 1 # No longer allow disabling the device tree since it is required and can prevent the Pi from booting + # CURRENT_SETTING="enabled" # assume not disabled + # DEFAULT= + # if [ -e $CONFIG ] && grep -q "^device_tree=$" $CONFIG; then + # CURRENT_SETTING="disabled" + # DEFAULT=--defaultno + # fi + # RET=${args[1]} + # if [ $RET -eq 0 ]; then + # sed $CONFIG -i -e "s/^\(device_tree=\)$/#\1/" + # sed $CONFIG -i -e "s/^#\(device_tree=.\)/\1/" + # SETTING=enabled + # elif [ $RET -eq 1 ]; then + # sed $CONFIG -i -e "s/^#\(device_tree=\)$/\1/" + # sed $CONFIG -i -e "s/^\(device_tree=.\)/#\1/" + # if ! grep -q "^device_tree=$" $CONFIG; then + # printf "device_tree=\n" >> $CONFIG + # fi + # SETTING=disabled + # else + # return 0 + # fi + # TENSE=is + # REBOOT= + # if [ $SETTING != $CURRENT_SETTING ]; then + # TENSE="will be" + # REBOOT=" after a reboot" + # ASK_TO_REBOOT=1 + # fi } get_devicetree() { @@ -524,27 +524,77 @@ get_spi() { fi } -do_serial() { - CURRENT_STATUS="yes" # assume ttyAMA0 output enabled - if ! grep -q "^T.*:.*:respawn:.*ttyAMA0" /etc/inittab; then - CURRENT_STATUS="no" +get_serial() { + if grep -q -E "console=(serial0|ttyAMA0|ttyS0)" $CMDLINE ; then + echo 0 + else + echo 1 fi +} - #"Would you like a login shell to be accessible over serial?" +get_serial_hw() { + if grep -q -E "^enable_uart=1" $CONFIG ; then + echo 0 + elif grep -q -E "^enable_uart=0" $CONFIG ; then + echo 1 + elif [ -e /dev/serial0 ] ; then + echo 0 + else + echo 1 + fi +} + +do_serial() { + CMDLINE=/boot/cmdline.txt + DEFAULTS=--defaultno + DEFAULTH=--defaultno + CURRENTS=0 + CURRENTH=0 + + if [ $(get_serial) -eq 0 ]; then + DEFAULTS= + CURRENTS=1 + fi + if [ $(get_serial_hw) -eq 0 ]; then + DEFAULTH= + CURRENTH=1 + fi RET=${args[1]} - if [ $RET -eq 1 ]; then - sed -i /etc/inittab -e "s|^.*:.*:respawn:.*ttyAMA0|#&|" - sed -i /boot/cmdline.txt -e "s/console=ttyAMA0,[0-9]\+ //" - #"Serial is now disabled" - elif [ $RET -eq 0 ]; then - sed -i /etc/inittab -e "s|^#\(.*:.*:respawn:.*ttyAMA0\)|\1|" - if ! grep -q "^T.*:.*:respawn:.*ttyAMA0" /etc/inittab; then - printf "T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100\n" >> /etc/inittab + if [ $RET -eq $CURRENTS ]; then + ASK_TO_REBOOT=1 + fi + + if [ $RET -eq 0 ]; then + if grep -q "console=ttyAMA0" $CMDLINE ; then + if [ -e /proc/device-tree/aliases/serial0 ]; then + sed -i $CMDLINE -e "s/console=ttyAMA0/console=serial0/" + fi + elif ! grep -q "console=ttyAMA0" $CMDLINE && ! grep -q "console=serial0" $CMDLINE ; then + if [ -e /proc/device-tree/aliases/serial0 ]; then + sed -i $CMDLINE -e "s/root=/console=serial0,115200 root=/" + else + sed -i $CMDLINE -e "s/root=/console=ttyAMA0,115200 root=/" + fi fi - if ! grep -q "console=ttyAMA0" /boot/cmdline.txt; then - sed -i /boot/cmdline.txt -e "s/root=/console=ttyAMA0,115200 root=/" + set_config_var enable_uart 1 $CONFIG + SSTATUS=enabled + HSTATUS=enabled + elif [ $RET -eq 1 ]; then + sed -i $CMDLINE -e "s/console=ttyAMA0,[0-9]\+ //" + sed -i $CMDLINE -e "s/console=serial0,[0-9]\+ //" + SSTATUS=disabled + if [ $RET -eq $CURRENTH ]; then + ASK_TO_REBOOT=1 + fi + if [ $RET -eq 0 ]; then + set_config_var enable_uart 1 $CONFIG + HSTATUS=enabled + elif [ $RET -eq 1 ]; then + set_config_var enable_uart 0 $CONFIG + HSTATUS=disabled + else + return $RET fi - #"Serial is now enabled" else return $RET fi @@ -561,18 +611,6 @@ get_camera() { echo $OUTPUT } -get_serial() { - if grep -q -E "^enable_uart=1" $CONFIG ; then - echo 0 - elif grep -q -E "^enable_uart=0" $CONFIG ; then - echo 1 - elif [ -e /dev/serial0 ] ; then - echo 0 - else - echo 1 - fi -} - get_w1(){ if grep -q -E "^dtoverlay=w1-gpio" $CONFIG; then echo 0 @@ -618,7 +656,7 @@ case "$FUN" in 15) get_timezone ;; 16) do_timezone ;; 17) get_camera ;; - 18) get_serial ;; + 18) get_serial_hw ;; 19) do_w1 ;; 20) get_w1 ;; 21) get_i2c ;; From e881df4c6da83cf5340d820ffd9abbb8f40993cf Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 23 Jan 2018 17:10:07 -0700 Subject: [PATCH 111/129] Prevent uninstall subprocess from exiting when the service is stopped. --- myDevices/cloud/client.py | 6 ++++-- myDevices/utils/subprocess.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index 3341744..ef671be 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -422,7 +422,7 @@ def ProcessAgentCommand(self, message): error = None try: if message['suffix'] == 'uninstall': - output, result = executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh') + output, result = executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh', disablePipe=True) debug('ProcessAgentCommand: {}, result: {}, output: {}'.format(message, result, output)) if result != 0: error = 'Error uninstalling agent' @@ -434,6 +434,8 @@ def ProcessAgentCommand(self, message): # else: # info('Set config item: {} {}'.format(key, value)) # self.config.set('Agent', key, value) + else: + error = 'Unknown agent command: {}'.format(message['suffix']) except Exception as ex: error = '{}: {}'.format(type(ex).__name__, ex) self.EnqueueCommandResponse(message, error) @@ -495,7 +497,7 @@ def ProcessDeviceCommand(self, message): elif message['suffix'] == 'delete': result = self.sensorsClient.RemoveSensor(payload['sensorId']) else: - info('Unknown device command: {}'.format(message['suffix'])) + error = 'Unknown device command: {}'.format(message['suffix']) debug('ProcessDeviceCommand result: {}'.format(result)) if result is False: error = 'Device command failed' diff --git a/myDevices/utils/subprocess.py b/myDevices/utils/subprocess.py index d5d501d..eb0c914 100644 --- a/myDevices/utils/subprocess.py +++ b/myDevices/utils/subprocess.py @@ -1,7 +1,7 @@ """ This module contains functions for launching subprocesses and returning output from them. """ -from subprocess import Popen, PIPE +from subprocess import Popen, PIPE, DEVNULL from myDevices.utils.logger import debug, info, error, exception def setMemoryLimits(): @@ -13,16 +13,20 @@ def setMemoryLimits(): except: pass -def executeCommand(command, increaseMemoryLimit=False): +def executeCommand(command, increaseMemoryLimit=False, disablePipe=False): """Execute a specified command, increasing the processes memory limits if specified""" debug('executeCommand: ' + command) output = '' returncode = 1 try: - setLimit = None + preexec = None + pipe = PIPE if increaseMemoryLimit: - setLimit = setMemoryLimits - process = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, preexec_fn=setLimit) + preexec = setMemoryLimits + if disablePipe: + debug('Disable pipe to prevent child exiting when parent exits') + pipe = DEVNULL + process = Popen(command, stdout=pipe, stderr=pipe, shell=True, preexec_fn=preexec) (stdout_data, stderr_data) = process.communicate() returncode = process.wait() returncode = process.returncode From 510a226a4a73889c4f3ebd35d6db80363c4bd125 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 23 Jan 2018 17:54:56 -0700 Subject: [PATCH 112/129] Use new JSON command payload format. --- myDevices/cloud/cayennemqtt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 3cb635f..1134bd8 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -159,10 +159,11 @@ def message_callback(self, client, userdata, msg): """ try: message = {} - if msg.topic[-len(COMMAND_JSON_TOPIC):] == COMMAND_JSON_TOPIC: - message['payload'] = loads(msg.payload.decode()) - message['cmdId'] = message['payload']['cmdId'] - channel = message['payload']['channel'].split('/')[-1].split(';') + if msg.topic[-len(COMMAND_JSON_TOPIC):] == COMMAND_JSON_TOPIC: + payload = loads(msg.payload.decode()) + message['payload'] = payload['value'] + message['cmdId'] = payload['cmdId'] + channel = payload['channel'].split('/')[-1].split(';') else: payload = msg.payload.decode().split(',') if len(payload) > 1: From d9ee48b641ceb20e0b92be3caaee4086fa5829c6 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 1 Mar 2018 14:43:32 -0700 Subject: [PATCH 113/129] Update version and setup classifiers. --- myDevices/__init__.py | 2 +- setup.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/myDevices/__init__.py b/myDevices/__init__.py index c6a0fa5..295c145 100644 --- a/myDevices/__init__.py +++ b/myDevices/__init__.py @@ -1,4 +1,4 @@ """ This package contains the Cayenne agent, which is a full featured client for the Cayenne IoT project builder: https://cayenne.mydevices.com. It sends system information as well as sensor and actuator data and responds to actuator messages initiated from the Cayenne dashboard and mobile apps. """ -__version__ = '1.1.0' +__version__ = '2.0.0' diff --git a/setup.py b/setup.py index 68eceba..fbea1cb 100644 --- a/setup.py +++ b/setup.py @@ -6,14 +6,15 @@ from myDevices.system.hardware import Hardware -classifiers = ['Development Status :: 1 - Alpha', +classifiers = ['Development Status :: 5 - Production/Stable', 'Operating System :: POSIX :: Linux', 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3', 'Topic :: Software Development', 'Topic :: Home Automation', - 'Topic :: System :: Hardware'] + 'Topic :: System :: Hardware', + 'Topic :: System :: Monitoring'] try: # Try to install under the cayenne user, if it exists. From 78df1be0814b581e9ed203347276c9e1bf80c715 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 6 Mar 2018 18:01:28 -0700 Subject: [PATCH 114/129] Auto-detect 1-wire devices. --- myDevices/sensors/sensors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 62bde51..c4371ed 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -146,6 +146,7 @@ def BusInfo(self): def SensorsInfo(self): """Return a list with current sensor states for all enabled sensors""" + manager.deviceDetector() devices = manager.getDeviceList() sensors_info = [] if devices is None: From 6d12b0df06026fc91e3617a0f87f3cfdd652dea8 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 7 Mar 2018 12:02:54 -0700 Subject: [PATCH 115/129] Set display name so auto-detected 1-wire devices show a default name. --- myDevices/sensors/sensors.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index c4371ed..14412d7 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -167,8 +167,12 @@ def SensorsInfo(self): extension_types = {'ADC': {'function': 'analogReadAllFloat'}, 'DAC': {'function': 'analogReadAllFloat'}, 'PWM': {'function': 'pwmWildcard'}, - 'DAC': {'function': 'wildcard'}} + 'GPIOPort': {'function': 'wildcard'}} for device_type in device['type']: + try: + display_name = device['description'] + except: + display_name = None if device_type in sensor_types: try: sensor_type = sensor_types[device_type] @@ -177,7 +181,7 @@ def SensorsInfo(self): channel = '{}:{}'.format(device['name'], device_type.lower()) else: channel = device['name'] - cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, channel, value=self.CallDeviceFunction(func), **sensor_type['data_args']) + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, channel, value=self.CallDeviceFunction(func), **sensor_type['data_args'], name=display_name) except: exception('Failed to get sensor data: {} {}'.format(device_type, device['name'])) else: @@ -186,7 +190,7 @@ def SensorsInfo(self): func = getattr(sensor, extension_type['function']) values = self.CallDeviceFunction(func) for pin, value in values.items(): - cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['name'] + ':' + str(pin), cayennemqtt.VALUE, value) + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, device['name'] + ':' + str(pin), cayennemqtt.VALUE, value, name=display_name) except: exception('Failed to get extension data: {} {}'.format(device_type, device['name'])) logJson('Sensors info: {}'.format(sensors_info)) From 979c742029550fab59c58d4f95ec34381f27e456 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 7 Mar 2018 12:03:44 -0700 Subject: [PATCH 116/129] Use mutex when updating device info. --- myDevices/devices/manager.py | 165 ++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 79 deletions(-) diff --git a/myDevices/devices/manager.py b/myDevices/devices/manager.py index 28f03fc..19155f3 100644 --- a/myDevices/devices/manager.py +++ b/myDevices/devices/manager.py @@ -2,6 +2,7 @@ import os.path import json as JSON from time import sleep, time +from threading import RLock from myDevices.utils import logger from myDevices.utils import types from myDevices.utils.config import Config @@ -12,6 +13,9 @@ PACKAGES = [serial, digital, analog, sensor, shield] DYNAMIC_DEVICES = {} DEVICES_JSON_FILE = "/etc/myDevices/devices.json" + +mutex = RLock() + def deviceDetector(): logger.debug('deviceDetector') try: @@ -28,7 +32,6 @@ def deviceDetector(): saveDevice(dev['name'], int(time())) except Exception as e: logger.error("Device detector: %s" % e) - # sleep(5) def findDeviceClass(name): for package in PACKAGES: @@ -47,34 +50,36 @@ def findDeviceClass(name): return None def saveDevice(name, install_date): - logger.debug('saveDevice: ' + str(name)) - if name not in DEVICES: - return - #never save to json devices that are manually added - if DEVICES[name]['origin'] == 'manual': - return - DYNAMIC_DEVICES[name] = DEVICES[name] - DEVICES[name]['install_date'] = install_date - json_devices = getJSON(DYNAMIC_DEVICES) - with open(DEVICES_JSON_FILE, 'w') as outfile: - outfile.write(json_devices) + with mutex: + logger.debug('saveDevice: ' + str(name)) + if name not in DEVICES: + return + #never save to json devices that are manually added + if DEVICES[name]['origin'] == 'manual': + return + DYNAMIC_DEVICES[name] = DEVICES[name] + DEVICES[name]['install_date'] = install_date + json_devices = getJSON(DYNAMIC_DEVICES) + with open(DEVICES_JSON_FILE, 'w') as outfile: + outfile.write(json_devices) def removeDevice(name): - if name in DEVICES: - if name in DYNAMIC_DEVICES: - if hasattr(DEVICES[name]["device"], 'close'): - DEVICES[name]["device"].close() - del DEVICES[name] - del DYNAMIC_DEVICES[name] - json_devices = getJSON(DYNAMIC_DEVICES) - with open(DEVICES_JSON_FILE, 'w') as outfile: - outfile.write(json_devices) - logger.debug("Deleted device %s" % name) - return (200, None, None) - logger.error("Cannot delete %s, found but not added via REST" % name) - return (403, None, None) - logger.error("Cannot delete %s, not found" % name) - return (404, None, None) + with mutex: + if name in DEVICES: + if name in DYNAMIC_DEVICES: + if hasattr(DEVICES[name]["device"], 'close'): + DEVICES[name]["device"].close() + del DEVICES[name] + del DYNAMIC_DEVICES[name] + json_devices = getJSON(DYNAMIC_DEVICES) + with open(DEVICES_JSON_FILE, 'w') as outfile: + outfile.write(json_devices) + logger.debug("Deleted device %s" % name) + return (200, None, None) + logger.error("Cannot delete %s, found but not added via REST" % name) + return (403, None, None) + logger.error("Cannot delete %s, not found" % name) + return (404, None, None) def addDeviceJSON(json): @@ -105,57 +110,58 @@ def addDeviceJSON(json): return (500, "ERROR", "text/plain") def updateDevice(name, json): - if not name in DEVICES: - return (404, None, None) + with mutex: + if not name in DEVICES: + return (404, None, None) - if "name" in json: -# forbid name changed -# if json["name"] != name: -# return (403, "FORBIDDEN", "text/plain") - - if json["name"] != name and json["name"] in DEVICES: - return (403, "ALREADY_EXISTS", "text/plain") + if "name" in json: + # forbid name changed + # if json["name"] != name: + # return (403, "FORBIDDEN", "text/plain") - logger.info("Edit %s" % name) - (c, d, t) = removeDevice(name) - if c == 200: - (c, d, t) = addDeviceJSON(json) - - return (c, d, t) + if json["name"] != name and json["name"] in DEVICES: + return (403, "ALREADY_EXISTS", "text/plain") + logger.info("Edit %s" % name) + (c, d, t) = removeDevice(name) + if c == 200: + (c, d, t) = addDeviceJSON(json) + + return (c, d, t) def addDevice(name, device, description, args, origin): - if name in DEVICES: - logger.error("Device <%s> already exists" % name) - return -1 - logger.debug('addDevice: ' + str(name) + ' ' + str(device)) -# if '/' in device: -# deviceClass = device.split('/')[0] -# else: -# deviceClass = device - try: - constructor = findDeviceClass(device) - except Exception as ex: - logger.debug('findDeviceClass failure:' + str(ex)) - return 0 - logger.debug('constructor class found ' + str(constructor)) - if constructor == None: - raise Exception("Device driver not found for %s" % device) + with mutex: + if name in DEVICES: + logger.error("Device <%s> already exists" % name) + return -1 + logger.debug('addDevice: ' + str(name) + ' ' + str(device)) + # if '/' in device: + # deviceClass = device.split('/')[0] + # else: + # deviceClass = device + try: + constructor = findDeviceClass(device) + except Exception as ex: + logger.debug('findDeviceClass failure:' + str(ex)) + return 0 + logger.debug('constructor class found ' + str(constructor)) + if constructor == None: + raise Exception("Device driver not found for %s" % device) - instance = None - try: - if len(args) > 0: - instance = constructor(**args) - else: - instance = constructor() - logger.debug('Adding instance ' + str(instance)) - addDeviceInstance(name, device, description, instance, args, origin) - return 1 - except Exception as e: - logger.error("Error while adding device %s(%s) : %s" % (name, device, e)) - # addDeviceInstance(name, device, description, None, args, origin) - removeDevice(name) - return 0 + instance = None + try: + if len(args) > 0: + instance = constructor(**args) + else: + instance = constructor() + logger.debug('Adding instance ' + str(instance)) + addDeviceInstance(name, device, description, instance, args, origin) + return 1 + except Exception as e: + logger.error("Error while adding device %s(%s) : %s" % (name, device, e)) + # addDeviceInstance(name, device, description, None, args, origin) + removeDevice(name) + return 0 def addDeviceConf(devices, origin): for (name, params) in devices: @@ -222,13 +228,14 @@ def addDeviceInstance(name, device, description, instance, args, origin): } def closeDevices(): - devices = [k for k in DEVICES.keys()] - for name in devices: - device = DEVICES[name]["device"] - logger.debug("Closing device %s - %s" % (name, device)) - del DEVICES[name] - if hasattr(device, 'close'): - device.close() + with mutex: + devices = [k for k in DEVICES.keys()] + for name in devices: + device = DEVICES[name]["device"] + logger.debug("Closing device %s - %s" % (name, device)) + del DEVICES[name] + if hasattr(device, 'close'): + device.close() def getJSON(devices_list): return types.jsonDumps(getDeviceList(devices_list)) From 058d4911fd3b49127436d4bd40c9725ef7d8b4c3 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 9 Mar 2018 14:22:44 -0700 Subject: [PATCH 117/129] Exit with error if prompted for sudo password on uninstall. --- myDevices/cloud/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index ef671be..e498400 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -422,7 +422,7 @@ def ProcessAgentCommand(self, message): error = None try: if message['suffix'] == 'uninstall': - output, result = executeCommand('sudo /etc/myDevices/uninstall/uninstall.sh', disablePipe=True) + output, result = executeCommand('sudo -n /etc/myDevices/uninstall/uninstall.sh', disablePipe=True) debug('ProcessAgentCommand: {}, result: {}, output: {}'.format(message, result, output)) if result != 0: error = 'Error uninstalling agent' From ac4b30876e3aa48c7f538bfcbfaaea6cfcf33771 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 12 Mar 2018 15:22:04 -0600 Subject: [PATCH 118/129] Add types and units for system info. --- myDevices/cloud/apiclient.py | 2 +- myDevices/cloud/client.py | 2 +- myDevices/cloud/download_speed.py | 6 ++--- myDevices/system/systeminfo.py | 44 +++++++++++++++++++++---------- myDevices/test/systeminfo_test.py | 14 +++++++--- 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/myDevices/cloud/apiclient.py b/myDevices/cloud/apiclient.py index 6ec089c..3a8eb18 100644 --- a/myDevices/cloud/apiclient.py +++ b/myDevices/cloud/apiclient.py @@ -60,7 +60,7 @@ def getMessageBody(self, inviteCode): capacity_data = system_info.getMemoryInfo((cayennemqtt.CAPACITY,)) capacity_data += system_info.getDiskInfo((cayennemqtt.CAPACITY,)) for item in capacity_data: - cayennemqtt.DataChannel.add(system_data, item['channel'], value=item['value'], type='memory', unit='byte') + system_data.append(item) body['properties'] = {} body['properties']['pinmap'] = NativeGPIO().MAPPING if system_data: diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index e498400..db3f9ae 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -266,7 +266,7 @@ def SendSystemState(self): data = [] download_speed = self.downloadSpeed.getDownloadSpeed() if download_speed: - cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed) + cayennemqtt.DataChannel.add(data, cayennemqtt.SYS_NET, suffix=cayennemqtt.SPEEDTEST, value=download_speed, type='bw', unit='mbps') data += self.sensorsClient.systemData info('Send system state: {} items'.format(len(data))) self.EnqueuePacket(data) diff --git a/myDevices/cloud/download_speed.py b/myDevices/cloud/download_speed.py index 65c5264..7ab5eba 100644 --- a/myDevices/cloud/download_speed.py +++ b/myDevices/cloud/download_speed.py @@ -49,17 +49,17 @@ def Test(self): def TestDownload(self): """Test download speed by retrieving a file""" try: - a = datetime.now() info('Executing regular download test for network speed') url = self.config.get('Agent', 'DownloadSpeedTestUrl', defaultUrl) debug(url + ' ' + download_path) + a = datetime.now() request.urlretrieve(url, download_path) - request.urlcleanup() b = datetime.now() + request.urlcleanup() c = b - a if path.exists(download_path): size = path.getsize(download_path)/mb - self.downloadSpeed = size/c.total_seconds() + self.downloadSpeed = size/c.total_seconds() * 8 #Convert to megabits per second remove(download_path) return True except socket_error as serr: diff --git a/myDevices/system/systeminfo.py b/myDevices/system/systeminfo.py index 40ea0f8..caeed8a 100644 --- a/myDevices/system/systeminfo.py +++ b/myDevices/system/systeminfo.py @@ -31,16 +31,20 @@ def getCpuInfo(self): [{ 'channel': 'sys:cpu;load', - 'value': 12.8 + 'value': 12.8, + 'type': 'cpuload', + 'unit': 'p' }, { 'channel': 'sys:cpu;temp', - 'value': 50.843 + 'value': 50.843, + 'type': 'temp', + 'unit': 'c' }] """ cpu_info = [] try: - cayennemqtt.DataChannel.add(cpu_info, cayennemqtt.SYS_CPU, suffix=cayennemqtt.LOAD, value=psutil.cpu_percent(1)) - cayennemqtt.DataChannel.add(cpu_info, cayennemqtt.SYS_CPU, suffix=cayennemqtt.TEMPERATURE, value=CpuInfo.get_cpu_temp()) + cayennemqtt.DataChannel.add(cpu_info, cayennemqtt.SYS_CPU, suffix=cayennemqtt.LOAD, value=psutil.cpu_percent(1), type='cpuload', unit='p') + cayennemqtt.DataChannel.add(cpu_info, cayennemqtt.SYS_CPU, suffix=cayennemqtt.TEMPERATURE, value=CpuInfo.get_cpu_temp(), type='temp', unit='c') except: exception('Error getting CPU info') return cpu_info @@ -55,19 +59,23 @@ def getMemoryInfo(self, types): [{ 'channel': 'sys:ram;capacity', - 'value': 968208384 + 'value': 968208384, + 'type': 'memory', + 'type': 'b' }, { 'channel': 'sys:ram;usage', - 'value': 296620032 + 'value': 296620032, + 'type': 'memory', + 'type': 'b' }] """ memory_info = [] try: vmem = psutil.virtual_memory() if not types or cayennemqtt.USAGE in types: - cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.USAGE, value=vmem.total - vmem.available) + cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.USAGE, value=vmem.total - vmem.available, type='memory', unit='b') if not types or cayennemqtt.CAPACITY in types: - cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.CAPACITY, value=vmem.total) + cayennemqtt.DataChannel.add(memory_info, cayennemqtt.SYS_RAM, suffix=cayennemqtt.CAPACITY, value=vmem.total, type='memory', unit='b') except: exception('Error getting memory info') return memory_info @@ -82,16 +90,24 @@ def getDiskInfo(self, types): [{ 'channel': 'sys:storage:/;capacity', - 'value': 13646516224 + 'value': 13646516224, + 'type': 'memory', + 'type': 'b' }, { 'channel': 'sys:storage:/;usage', - 'value': 6353821696 + 'value': 6353821696, + 'type': 'memory', + 'type': 'b' }, { 'channel': 'sys:storage:/mnt/cdrom;capacity', - 'value': 479383552 + 'value': 479383552, + 'type': 'memory', + 'type': 'b' }, { 'channel': 'sys:storage:/mnt/cdrom;usage', - 'value': 0 + 'value': 0, + 'type': 'memory', + 'type': 'b' }] """ storage_info = [] @@ -102,9 +118,9 @@ def getDiskInfo(self, types): usage = psutil.disk_usage(partition.mountpoint) if usage.total: if not types or cayennemqtt.USAGE in types: - cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used) + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.USAGE, usage.used, type='memory', unit='b') if not types or cayennemqtt.CAPACITY in types: - cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total) + cayennemqtt.DataChannel.add(storage_info, cayennemqtt.SYS_STORAGE, partition.mountpoint, cayennemqtt.CAPACITY, usage.total, type='memory', unit='b') except: pass except: diff --git a/myDevices/test/systeminfo_test.py b/myDevices/test/systeminfo_test.py index 93b892c..ad19abc 100644 --- a/myDevices/test/systeminfo_test.py +++ b/myDevices/test/systeminfo_test.py @@ -5,18 +5,24 @@ class SystemInfoTest(unittest.TestCase): def setUp(self): - # setInfo() + setInfo() system_info = SystemInfo() - self.info = {item['channel']:item['value'] for item in system_info.getSystemInformation()} + self.info = {item['channel']:item for item in system_info.getSystemInformation()} info(self.info) def testSystemInfo(self): self.assertIn('sys:cpu;load', self.info) + self.assertEqual(self.info['sys:cpu;load']['type'], 'cpuload') + self.assertEqual(self.info['sys:cpu;load']['unit'], 'p') self.assertIn('sys:cpu;temp', self.info) + self.assertEqual(self.info['sys:cpu;temp']['type'], 'temp') + self.assertEqual(self.info['sys:cpu;temp']['unit'], 'c') self.assertIn('sys:ram;usage', self.info) - # self.assertIn('sys:ram;capacity', self.info) + self.assertEqual(self.info['sys:ram;usage']['type'], 'memory') + self.assertEqual(self.info['sys:ram;usage']['unit'], 'b') self.assertIn('sys:storage:/;usage', self.info) - # self.assertIn('sys:storage:/;capacity', self.info) + self.assertEqual(self.info['sys:storage:/;usage']['type'], 'memory') + self.assertEqual(self.info['sys:storage:/;usage']['unit'], 'b') self.assertIn('sys:net;ip', self.info) # self.assertIn('sys:net;ssid', self.info) From 6fa9300959587e702280df29d4ce2b0846306cd0 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Tue, 27 Mar 2018 12:49:56 -0600 Subject: [PATCH 119/129] Fix response message format. --- myDevices/cloud/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index db3f9ae..d20aa75 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -509,9 +509,10 @@ def EnqueueCommandResponse(self, message, error): """Send response after processing a command message""" debug('EnqueueCommandResponse error: {}'.format(error)) if error: - response = '{},error={}'.format(message['cmdId'], error) + response = 'error,{}={}'.format(message['cmdId'], error) else: - response = '{},ok'.format(message['cmdId']) + response = 'ok,{}'.format(message['cmdId']) + info(response) self.EnqueuePacket(response, cayennemqtt.COMMAND_RESPONSE_TOPIC) def EnqueuePacket(self, message, topic=cayennemqtt.DATA_TOPIC): From 0f966c8fcf99065100ae104646bab25a78b5f5c9 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Wed, 28 Mar 2018 18:56:39 -0600 Subject: [PATCH 120/129] Split BMP180 and VCNL4000 into component sensors. --- myDevices/devices/i2c.py | 11 ++++-- myDevices/devices/sensor/__init__.py | 4 +-- myDevices/devices/sensor/bmp085.py | 33 ++++++++++++++---- myDevices/devices/sensor/vcnl4000.py | 51 ++++++++++++++++++++++------ 4 files changed, 77 insertions(+), 22 deletions(-) diff --git a/myDevices/devices/i2c.py b/myDevices/devices/i2c.py index c105c50..eea281f 100644 --- a/myDevices/devices/i2c.py +++ b/myDevices/devices/i2c.py @@ -45,10 +45,10 @@ SLAVES = [None for i in range(128)] class I2C(Bus): - def __init__(self, slave): + def __init__(self, slave, allow_duplicates=False): global SLAVES - if SLAVES[slave] != None: + if SLAVES[slave] != None and not allow_duplicates: raise Exception("SLAVE_ADDRESS_USED") self.channel = 0 @@ -63,7 +63,12 @@ def __init__(self, slave): if fcntl.ioctl(self.fd, I2C_SLAVE, self.slave): raise Exception("Error binding I2C slave 0x%02X" % self.slave) - SLAVES[self.slave] = self + # Since we now allow duplicates, e.g. BMP180_TEMPERATURE & BMP180_PRESSURE, we might need to + # change the SLAVES list to store a reference count or base class name as a way of making sure + # only the same base sensor type is duplicated. That doesn't seem to be an issue currently though + # so for now we just ignore the SLAVES check. + if not allow_duplicates: + SLAVES[self.slave] = self def __str__(self): return "I2C(slave=0x%02X)" % self.slave diff --git a/myDevices/devices/sensor/__init__.py b/myDevices/devices/sensor/__init__.py index 2a453e9..9d3e4c2 100644 --- a/myDevices/devices/sensor/__init__.py +++ b/myDevices/devices/sensor/__init__.py @@ -187,10 +187,10 @@ def getHumidityPercent(self): return self.__getHumidity__() * 100 DRIVERS = {} -DRIVERS["bmp085"] = ["BMP085", "BMP180"] +DRIVERS["bmp085"] = ["BMP085", "BMP180", "BMP180_TEMPERATURE", "BMP180_PRESSURE"] DRIVERS["onewiretemp"] = ["DS1822", "DS1825", "DS18B20", "DS18S20", "DS28EA00"] DRIVERS["tmpXXX"] = ["TMP36", "TMP75", "TMP102", "TMP275"] DRIVERS["tslXXXX"] = ["TSL2561", "TSL2561CS", "TSL2561T", "TSL4531", "TSL45311", "TSL45313", "TSL45315", "TSL45317"] -DRIVERS["vcnl4000"] = ["VCNL4000"] +DRIVERS["vcnl4000"] = ["VCNL4000", "VCNL4000_LUMINOSITY", "VCNL4000_DISTANCE"] DRIVERS["hytXXX"] = ["HYT221"] diff --git a/myDevices/devices/sensor/bmp085.py b/myDevices/devices/sensor/bmp085.py index e1083b2..a87c2bf 100644 --- a/myDevices/devices/sensor/bmp085.py +++ b/myDevices/devices/sensor/bmp085.py @@ -18,9 +18,12 @@ from myDevices.devices.sensor import Temperature, Pressure class BMP085(I2C, Temperature, Pressure): - def __init__(self, altitude=0, external=None): - I2C.__init__(self, 0x77) - Pressure.__init__(self, altitude, external) + def __init__(self, altitude=0, external=None, temperature=True, pressure=True): + self.temperature = temperature + self.pressure = pressure + I2C.__init__(self, 0x77, True) + if self.pressure: + Pressure.__init__(self, altitude, external) self.ac1 = self.readSignedInteger(0xAA) self.ac2 = self.readSignedInteger(0xAC) @@ -38,7 +41,12 @@ def __str__(self): return "BMP085" def __family__(self): - return [Temperature.__family__(self), Pressure.__family__(self)] + family = [] + if self.temperature: + family.append(Temperature.__family__(self)) + if self.pressure: + family.append(Pressure.__family__(self)) + return family def readUnsignedInteger(self, address): d = self.readRegisters(address, 2) @@ -100,9 +108,22 @@ def __getPascal__(self): return int(p) class BMP180(BMP085): - def __init__(self, altitude=0, external=None): - BMP085.__init__(self, altitude, external) + def __init__(self, altitude=0, external=None, temperature=True, pressure=True): + BMP085.__init__(self, altitude, external, temperature, pressure) def __str__(self): return "BMP180" +class BMP180_TEMPERATURE(BMP180): + def __init__(self, altitude=0, external=None): + BMP180.__init__(self, altitude, external, temperature=True, pressure=False) + + def __str__(self): + return "BMP180_TEMPERATURE" + +class BMP180_PRESSURE(BMP180): + def __init__(self, altitude=0, external=None): + BMP180.__init__(self, altitude, external, temperature=False, pressure=True) + + def __str__(self): + return "BMP180_PRESSURE" \ No newline at end of file diff --git a/myDevices/devices/sensor/vcnl4000.py b/myDevices/devices/sensor/vcnl4000.py index cbbb378..c1b3e37 100644 --- a/myDevices/devices/sensor/vcnl4000.py +++ b/myDevices/devices/sensor/vcnl4000.py @@ -23,7 +23,7 @@ from myDevices.devices.i2c import I2C from myDevices.devices.sensor import Luminosity, Distance from myDevices.utils.types import toint -from myDevices.utils.logger import debug +from myDevices.utils.logger import debug class VCNL4000(I2C, Luminosity, Distance): REG_COMMAND = 0x80 @@ -52,8 +52,10 @@ class VCNL4000(I2C, Luminosity, Distance): MASK_PROX_READY = 0b00100000 MASK_AMB_READY = 0b01000000 - def __init__(self, slave=0b0010011, current=20, frequency=781, prox_threshold=15, prox_cycles=10, cal_cycles= 5): - I2C.__init__(self, toint(slave)) + def __init__(self, slave=0b0010011, current=20, frequency=781, prox_threshold=15, prox_cycles=10, cal_cycles=5, luminosity=True, distance=True): + self.luminosity = luminosity + self.distance = distance + I2C.__init__(self, toint(slave), True) self.setCurrent(toint(current)) self.setFrequency(toint(frequency)) self.prox_threshold = toint(prox_threshold) @@ -68,7 +70,12 @@ def __str__(self): return "VCNL4000(slave=0x%02X)" % self.slave def __family__(self): - return [Luminosity.__family__(self), Distance.__family__(self)] + family = [] + if self.luminosity: + family.append(Luminosity.__family__(self)) + if self.distance: + family.append(Distance.__family__(self)) + return family def __setProximityTiming__(self): self.writeRegister(self.REG_PROX_ADJUST, self.VAL_MOD_TIMING_DEF) @@ -85,12 +92,10 @@ def calibrate(self): self.offset = self.__measureOffset__() debug ("VCNL4000: offset = %d" % (self.offset)) return self.offset - def setCurrent(self, current): self.current = current self.__setCurrent__() - def getCurrent(self): return self.__getCurrent__() @@ -141,9 +146,20 @@ def __getCurrent__(self): return bits_current * 10 def __getLux__(self): - self.writeRegister(self.REG_COMMAND, self.VAL_START_AMB) - while not (self.readRegister(self.REG_COMMAND) & self.MASK_AMB_READY): - time.sleep(0.001) + count = 0 + while count < 5: + # This try catch loop is a hacky fix for issues when making an initial ambient light reading + # using a VCNL4010 sensor. It should be removed when true VCNL4010 support is added. + try: + self.writeRegister(self.REG_COMMAND, self.VAL_START_AMB) + while not (self.readRegister(self.REG_COMMAND) & self.MASK_AMB_READY): + time.sleep(0.001) + count = 10 + except OSError as ex: + count = count + 1 + time.sleep(0.05) + if count >= 5: + raise ex light_bytes = self.readRegisters(self.REG_AMB_RESULT_HIGH, 2) light_word = light_bytes[0] << 8 | light_bytes[1] return self.__calculateLux__(light_word) @@ -206,6 +222,19 @@ def __readProximityCounts__(self): while not (self.readRegister(self.REG_COMMAND) & self.MASK_PROX_READY): time.sleep(0.001) proximity_bytes = self.readRegisters(self.REG_PROX_RESULT_HIGH, 2) - debug ("VCNL4000: prox raw value = %d" % (proximity_bytes[0] << 8 | proximity_bytes[1])) + debug("VCNL4000: prox raw value = %d" % (proximity_bytes[0] << 8 | proximity_bytes[1])) return (proximity_bytes[0] << 8 | proximity_bytes[1]) - \ No newline at end of file + +class VCNL4000_LUMINOSITY(VCNL4000): + def __init__(self): + VCNL4000.__init__(self, luminosity=True, distance=False) + + def __str__(self): + return "VCNL4000_LUMINOSITY" + +class VCNL4000_DISTANCE(VCNL4000): + def __init__(self): + VCNL4000.__init__(self, luminosity=False, distance=True) + + def __str__(self): + return "VCNL4000_DISTANCE" \ No newline at end of file From a42d63b3a2ad10edfe7516e71f74413529d6c2aa Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 29 Mar 2018 11:57:45 -0600 Subject: [PATCH 121/129] Override SPI and I2c pin functions when they are enabled. --- myDevices/devices/digital/gpio.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/myDevices/devices/digital/gpio.py b/myDevices/devices/digital/gpio.py index 1ede7ee..35ce58d 100644 --- a/myDevices/devices/digital/gpio.py +++ b/myDevices/devices/digital/gpio.py @@ -337,6 +337,7 @@ def wildcard(self, compact=False): f = "function" v = "value" values = {} + self.system_config = SystemConfig.getConfig() for i in self.pins + self.overlay_pins: if compact: func = self.getFunction(i) @@ -360,9 +361,11 @@ def getFunctionString(self, channel): pass try: # On Raspberry Pis using the spi_bcm2835 driver SPI chip select is done via software rather than hardware - # so the pin function is OUT instead of ALT0. Here we override that if the SPI MOSI pin is set to ALT0 - # so the GPIO map in the UI will display the appropriate SPI pin info. - if channel in self.chip_select_pins and f == 1 and self.getFunction(self.spi_mosi_pin) == 4: + # so the pin function is OUT instead of ALT0. Here we override that (and the I2C to be safe) so the GPIO map + # in the UI will display the appropriate pin info. + if channel in self.spi_pins and self.system_config['SPI'] == 1: + function_string = functions[4] + if channel in self.i2c_pins and self.system_config['I2C'] == 1: function_string = functions[4] except: pass @@ -626,16 +629,17 @@ def setPinMapping(self): if isinstance(self.MAPPING, list): self.pins = [] self.overlay_pins = [] + self.spi_pins = [] + self.i2c_pins = [] + self.system_config = SystemConfig.getConfig() for header in self.MAPPING: self.pins.extend([pin['gpio'] for pin in header['map'] if 'gpio' in pin]) try: if Hardware().isRaspberryPi(): - if SystemConfig.getConfig()['OneWire'] == 1: + if self.system_config['OneWire'] == 1: self.overlay_pins.extend([pin['gpio'] for pin in header['map'] if 'overlay' in pin and pin['overlay']['channel'] == 'sys:1wire']) self.pins = [pin for pin in self.pins if pin not in self.overlay_pins] - self.chip_select_pins = [] - self.spi_mosi_pin = 10 - for header in self.MAPPING: - self.chip_select_pins.extend([pin['gpio'] for pin in header['map'] if 'alt0' in pin and pin['alt0']['name'] in ('CE0', 'CE1')]) + self.spi_pins.extend([pin['gpio'] for pin in header['map'] if 'alt0' in pin and pin['alt0']['channel'] == 'sys:spi']) + self.i2c_pins.extend([pin['gpio'] for pin in header['map'] if 'alt0' in pin and pin['alt0']['channel'] == 'sys:i2c']) except: pass From 2cf0f3ec26a7078d37408e052e4629c6e3604cae Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 30 Mar 2018 11:28:26 -0600 Subject: [PATCH 122/129] Use default value when suffix is omitted in GPIO & sensor commands. --- myDevices/cloud/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index d20aa75..bc4c252 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -459,7 +459,8 @@ def ProcessGpioCommand(self, message): error = None try: channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', '')) - result = self.sensorsClient.GpioCommand(message['suffix'], channel, message['payload']) + suffix = getattr(message, 'suffix', 'value') + result = self.sensorsClient.GpioCommand(suffix, channel, message['payload']) debug('ProcessGpioCommand result: {}'.format(result)) if result == 'failure': error = 'GPIO command failed' @@ -476,7 +477,8 @@ def ProcessSensorCommand(self, message): channel = None if len(sensor_info) > 1: channel = sensor_info[1] - result = self.sensorsClient.SensorCommand(message['suffix'], sensor, channel, message['payload']) + suffix = getattr(message, 'suffix', 'value') + result = self.sensorsClient.SensorCommand(suffix, sensor, channel, message['payload']) debug('ProcessSensorCommand result: {}'.format(result)) if result is False: error = 'Sensor command failed' From 0839dccdd9042e5eb26bb62fddf9b67ea01f0e04 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 30 Mar 2018 11:46:13 -0600 Subject: [PATCH 123/129] Fix incorrect use of getattr call. --- myDevices/cloud/client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/myDevices/cloud/client.py b/myDevices/cloud/client.py index bc4c252..0d5f973 100644 --- a/myDevices/cloud/client.py +++ b/myDevices/cloud/client.py @@ -459,8 +459,7 @@ def ProcessGpioCommand(self, message): error = None try: channel = int(message['channel'].replace(cayennemqtt.SYS_GPIO + ':', '')) - suffix = getattr(message, 'suffix', 'value') - result = self.sensorsClient.GpioCommand(suffix, channel, message['payload']) + result = self.sensorsClient.GpioCommand(message.get('suffix', 'value'), channel, message['payload']) debug('ProcessGpioCommand result: {}'.format(result)) if result == 'failure': error = 'GPIO command failed' @@ -477,8 +476,7 @@ def ProcessSensorCommand(self, message): channel = None if len(sensor_info) > 1: channel = sensor_info[1] - suffix = getattr(message, 'suffix', 'value') - result = self.sensorsClient.SensorCommand(suffix, sensor, channel, message['payload']) + result = self.sensorsClient.SensorCommand(message.get('suffix', 'value'), sensor, channel, message['payload']) debug('ProcessSensorCommand result: {}'.format(result)) if result is False: error = 'Sensor command failed' From 55013c59ea4f0729416945dfadb2c74c6f58655d Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 30 Mar 2018 12:32:34 -0600 Subject: [PATCH 124/129] Remove info message. --- myDevices/system/systemconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/system/systemconfig.py b/myDevices/system/systemconfig.py index 6399842..8c59d03 100644 --- a/myDevices/system/systemconfig.py +++ b/myDevices/system/systemconfig.py @@ -63,5 +63,5 @@ def getConfig(): config[name] = 1 - int(output.strip()) #Invert the value since the config script uses 0 for enable and 1 for disable except: exception('Get config') - info('SystemConfig: {}'.format(config)) + debug('SystemConfig: {}'.format(config)) return config From 31fa0e883cde2709e90413c6a3ec248dd834ae93 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Fri, 20 Apr 2018 12:18:51 -0600 Subject: [PATCH 125/129] Use new update script name. --- myDevices/cloud/updater.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/myDevices/cloud/updater.py b/myDevices/cloud/updater.py index fcb28b3..19f9bd0 100644 --- a/myDevices/cloud/updater.py +++ b/myDevices/cloud/updater.py @@ -10,7 +10,7 @@ from myDevices.utils.config import Config from myDevices.utils.subprocess import executeCommand -SETUP_NAME = 'myDevicesSetup_raspberrypi.sh' +SETUP_NAME = 'update_raspberrypi.sh' INSTALL_PATH = '/etc/myDevices/' UPDATE_PATH = INSTALL_PATH + 'updates/' UPDATE_CFG = UPDATE_PATH + 'updatecfg' @@ -52,7 +52,7 @@ def __init__(self, config, onUpdateConfig = None): self.Continue = True self.currentVersion = '' self.newVersion = '' - self.downloadUrl = '' + # self.downloadUrl = '' self.UpdateCleanup() self.startTime = datetime.now() - timedelta(days=1) @@ -146,9 +146,12 @@ def RetrieveUpdate(self): updateConfig = Config(UPDATE_CFG) try: self.newVersion = updateConfig.get('UPDATES','Version') - self.downloadUrl = updateConfig.get('UPDATES','Url') except: - error('Updater missing: update version or Url') + error('Updater missing version') + # try: + # self.downloadUrl = updateConfig.get('UPDATES','Url') + # except: + # error('Updater missing url') info('Updater retrieve update success') return True except: @@ -157,7 +160,7 @@ def RetrieveUpdate(self): def DownloadFile(self, url, localPath): try: - info( url + ' ' + localPath) + info(url + ' ' + localPath) with urlopen(url) as response: with open(localPath, 'wb') as output: output.write(response.read()) From 0f727fc8907f506f57942db56c585a8809765ab8 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 23 Apr 2018 16:53:52 -0600 Subject: [PATCH 126/129] Check if values are set to None to prevent issues with 0 values. --- myDevices/cloud/cayennemqtt.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/myDevices/cloud/cayennemqtt.py b/myDevices/cloud/cayennemqtt.py index 1134bd8..3f99e98 100644 --- a/myDevices/cloud/cayennemqtt.py +++ b/myDevices/cloud/cayennemqtt.py @@ -49,18 +49,18 @@ class DataChannel: def add(data_list, prefix, channel=None, suffix=None, value=None, type=None, unit=None, name=None): """Create data channel dict and append it to a list""" data_channel = prefix - if channel: + if channel is not None: data_channel += ':' + str(channel) - if suffix: + if suffix is not None: data_channel += ';' + str(suffix) data = {} data['channel'] = data_channel data['value'] = value - if type: + if type is not None: data['type'] = type - if unit: + if unit is not None: data['unit'] = unit - if name: + if name is not None: data['name'] = name data_list.append(data) From 684a7725f5aa9651adf7538f15822f88758825a1 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 30 Apr 2018 12:56:05 -0600 Subject: [PATCH 127/129] Overwrite config file during setup. --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index fbea1cb..3be63ce 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,9 @@ pass os.chown(directory, user_id, group_id) +# Touch config file so it overwrites older versions +os.utime('scripts/config.sh', None) + setup(name = 'myDevices', version = __version__, author = 'myDevices', From 3de05c098de4095b43389ee4ca4cab71d3d9f2d7 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Mon, 30 Apr 2018 13:04:10 -0600 Subject: [PATCH 128/129] Add new revision info. --- myDevices/system/hardware.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/myDevices/system/hardware.py b/myDevices/system/hardware.py index b79dd20..703e3f3 100644 --- a/myDevices/system/hardware.py +++ b/myDevices/system/hardware.py @@ -12,9 +12,9 @@ try: with open("/proc/cpuinfo") as f: - info = f.read() + cpuinfo = f.read() rc = re.compile("Revision\s*:\s(.*)\n") - result = rc.search(info) + result = rc.search(cpuinfo) if result: CPU_REVISION = result.group(1) if CPU_REVISION.startswith("1000"): @@ -28,7 +28,7 @@ else: BOARD_REVISION = 3 rc = re.compile("Hardware\s*:\s(.*)\n") - result = rc.search(info) + result = rc.search(cpuinfo) CPU_HARDWARE = result.group(1) except: exception("Error reading cpuinfo") @@ -39,7 +39,7 @@ class Hardware: def __init__(self): """Initialize board revision and model info""" - self.Revision = '0' + self.Revision = '0' self.Serial = None try: with open('/proc/cpuinfo','r') as f: @@ -68,20 +68,26 @@ def __init__(self): self.model = 'Raspberry Pi Compute Module' if self.Revision in ('0012', '0015'): self.model = 'Raspberry Pi Model A+' - if self.Revision in ('a01041', 'a21041', 'a22042'): + if self.Revision in ('a01040', 'a01041', 'a21041', 'a22042'): self.model = 'Raspberry Pi 2 Model B' - if self.Revision in ('900092', '900093'): + if self.Revision in ('900092', '900093', '920093'): self.model = 'Raspberry Pi Zero' if self.Revision in ('9000c1',): self.model = 'Raspberry Pi Zero W' - if self.Revision in ('a02082', 'a22082'): + if self.Revision in ('a02082', 'a22082', 'a32082'): self.model = 'Raspberry Pi 3 Model B' + if self.Revision in ('a020d3'): + self.model = 'Raspberry Pi 3 Model B+' + if self.Revision in ('a020a0'): + self.model = 'Raspberry Pi Compute Module 3' if 'Rockchip' in CPU_HARDWARE: self.model = 'Tinker Board' self.manufacturer = 'Element14/Premier Farnell' - if self.Revision in ('a01041', '900092', 'a02082', '0012', '0011', '0010', '000e', '0008', '0004'): + if self.Revision in ('a01041', '900092', 'a02082', '0012', '0011', '0010', '000e', '0008', '0004', 'a020d3', 'a01040', 'a020a0'): self.manufacturer = 'Sony, UK' - if self.Revision in ('0014', '0015', 'a21041', 'a22082'): + if self.Revision in ('a32082'): + self.manufacturer = 'Sony, Japan' + if self.Revision in ('0014', '0015', 'a21041', 'a22082', '920093'): self.manufacturer = 'Embest, China' if self.Revision in ('0005', '0009', '000f'): self.manufacturer = 'Qisda' From 1e7755074dff607dba8aafdf7409048135d75966 Mon Sep 17 00:00:00 2001 From: jburhenn Date: Thu, 3 May 2018 14:48:42 -0600 Subject: [PATCH 129/129] Fix syntax error. --- myDevices/sensors/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myDevices/sensors/sensors.py b/myDevices/sensors/sensors.py index 14412d7..2dd63d0 100644 --- a/myDevices/sensors/sensors.py +++ b/myDevices/sensors/sensors.py @@ -181,7 +181,7 @@ def SensorsInfo(self): channel = '{}:{}'.format(device['name'], device_type.lower()) else: channel = device['name'] - cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, channel, value=self.CallDeviceFunction(func), **sensor_type['data_args'], name=display_name) + cayennemqtt.DataChannel.add(sensors_info, cayennemqtt.DEV_SENSOR, channel, value=self.CallDeviceFunction(func), name=display_name, **sensor_type['data_args']) except: exception('Failed to get sensor data: {} {}'.format(device_type, device['name'])) else: