class SchedulerTest(unittest.TestCase): def setUp(self): self.maxDiff = None self.test_client = TestClient() self.test_engine = SchedulerEngine(self.test_client, 'test') self.schedule_events = [] def tearDown(self): self.remove_schedules() self.test_engine.stop() def add_schedules(self, schedule_events): for event in schedule_events: self.test_engine.add_scheduled_event(event, True) self.schedule_events = self.schedule_events + schedule_events def remove_schedules(self, engine=None): scheduled_events = { event['id']: event for event in self.schedule_events if 'id' in event } for event in scheduled_events.values(): self.assertTrue(self.test_engine.remove_scheduled_event(event)) def check_schedules_added(self, expected): actual = self.test_engine.get_scheduled_events() self.assertCountEqual(expected, actual) def check_schedules_run(self, expected, skip_jobs=()): print('Pause to allow scheduled events to execute') expected_to_run = [ action for event in expected if event['title'] not in skip_jobs for action in event['actions'] ] for i in range(70): time.sleep(1) if len(expected_to_run) > 0 and len(expected_to_run) == len( self.test_client.actions_ran): break self.assertCountEqual(expected_to_run, self.test_client.actions_ran) def test_missing_id(self): start_date = datetime.datetime.strftime( datetime.datetime.utcnow() + datetime.timedelta(seconds=60), '%Y-%m-%dT%H:%M:%S.%fZ') missing_id_event = { 'title': 'no_id_job', 'actions': ['no_id_job_action'], 'config': { 'type': 'date', 'start_date': start_date } } self.assertFalse( self.test_engine.add_scheduled_event(missing_id_event, True)) self.assertFalse(self.test_engine.get_scheduled_events()) def test_overwrite_job(self): start_date = datetime.datetime.strftime( datetime.datetime.utcnow() + datetime.timedelta(seconds=60), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'overwrite_1', 'title': 'overwritten_job', 'actions': ['overwritten_job_action'], 'config': { 'type': 'date', 'start_date': start_date } }, { 'id': 'overwrite_1', 'title': 'date_job_readd_same_id', 'actions': ['date_job_readd_same_id_action'], 'config': { 'type': 'date', 'start_date': start_date } }] self.add_schedules(schedule_events) expected = [ event for event in schedule_events if 'id' in event and event['title'] != 'overwritten_job' ] self.check_schedules_added(expected) def test_current_schedules(self): start_date = datetime.datetime.strftime( datetime.datetime.utcnow() + datetime.timedelta(seconds=60), '%Y-%m-%dT%H:%M:%S.%fZ') now = datetime.datetime.strftime(datetime.datetime.utcnow(), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'current_1', 'title': 'date_job', 'actions': ['date_job_action'], 'config': { 'type': 'date', 'start_date': start_date } }, { 'id': 'current_2', 'title': 'daily_job', 'actions': ['daily_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': start_date } }, { 'id': 'current_3', 'title': 'every_3_days_job', 'actions': ['every_3_days_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 3, 'start_date': start_date } }, { 'id': 'current_4', 'title': 'now_date_job', 'actions': ['now_date_job_action'], 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'current_5', 'title': 'weekly_job', 'actions': ['weekly_job_action'], 'config': { 'type': 'interval', 'unit': 'week', 'interval': 1, 'start_date': start_date } }, { 'id': 'current_6', 'title': 'bi-weekly_job', 'actions': ['weekly_job_action'], 'config': { 'type': 'interval', 'unit': 'week', 'interval': 2, 'start_date': start_date } }, { 'id': 'current_7', 'title': 'every_4_months_job', 'actions': ['every_4_months_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 4, 'start_date': start_date } }, { 'id': 'current_8', 'title': 'every_3_months_job', 'actions': ['every_3_months_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 3, 'start_date': now } }, { 'id': 'current_9', 'title': 'hourly_job', 'actions': ['hourly_job_action'], 'config': { 'type': 'interval', 'unit': 'hour', 'interval': 1, 'start_date': start_date } }] self.add_schedules(schedule_events) self.check_schedules_added(schedule_events) self.check_schedules_run(schedule_events) def test_past_schedules(self): next_minute = datetime.datetime.utcnow() + datetime.timedelta( seconds=60) passed_date = datetime.datetime.strftime( datetime.datetime.utcnow() - datetime.timedelta(seconds=120), '%Y-%m-%dT%H:%M:%S.%fZ') one_day_ago = datetime.datetime.strftime( next_minute - datetime.timedelta(days=1), '%Y-%m-%dT%H:%M:%S.%fZ') one_week_ago = datetime.datetime.strftime( next_minute - datetime.timedelta(days=7), '%Y-%m-%dT%H:%M:%S.%fZ') one_month_ago = datetime.datetime.strftime( schedule.month_delta(next_minute, -1), '%Y-%m-%dT%H:%M:%S.%fZ') one_year_ago = next_minute.replace(year=next_minute.year - 1) one_year_ago = datetime.datetime.strftime(one_year_ago, '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'past_1', 'title': 'expired_date_job', 'actions': ['expired_date_job_action'], 'config': { 'type': 'date', 'start_date': passed_date } }, { 'id': 'past_2', 'title': 'daily_job_started_one_day_ago', 'actions': ['daily_job_started_one_day_ago_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': one_day_ago } }, { 'id': 'past_3', 'title': 'monthly_job_started_one_month_ago', 'actions': ['monthly_job_started_one_month_ago_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': one_month_ago } }, { 'id': 'past_4', 'title': 'yearly_job_started_one_year_ago', 'actions': ['yearly_job_started_one_year_ago_action'], 'config': { 'type': 'interval', 'unit': 'year', 'interval': 1, 'start_date': one_year_ago } }, { 'id': 'past_5', 'title': 'every_2_years_job_started_one_year_ago', 'actions': ['every_2_years_job_started_one_year_ago_action'], 'config': { 'type': 'interval', 'unit': 'year', 'interval': 2, 'start_date': one_year_ago } }, { 'id': 'past_6', 'title': 'weekly_job_started_one_week_ago', 'actions': ['weekly_job_started_one_week_ago_action'], 'config': { 'type': 'interval', 'unit': 'week', 'interval': 1, 'start_date': one_week_ago } }] self.add_schedules(schedule_events) self.check_schedules_added(schedule_events) self.check_schedules_run( schedule_events, ('expired_date_job', 'every_2_years_job_started_one_year_ago')) def test_future_schedules(self): one_day_from_now = datetime.datetime.strftime( datetime.datetime.utcnow() + datetime.timedelta(days=1), '%Y-%m-%dT%H:%M:%S.%fZ') end_of_month = datetime.datetime.strftime( datetime.datetime(2015, 1, 31), '%Y-%m-%dT%H:%M:%S.%fZ') future_month = datetime.datetime.strftime( datetime.datetime(2017, 12, 31), '%Y-%m-%dT%H:%M:%S.%fZ') future_year = datetime.datetime.strftime(datetime.datetime(2017, 1, 1), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'future_1', 'title': 'daily_job_starts_one_day_from_now', 'actions': ['daily_job_starts_one_day_from_now_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': one_day_from_now } }, { 'id': 'future_2', 'title': 'end_of_month_job', 'actions': ['end_of_month_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': end_of_month } }, { 'id': 'future_3', 'title': 'future_month_job', 'actions': ['future_month_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': future_month } }, { 'id': 'future_4', 'title': 'future_year_job', 'actions': ['future_year_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': future_year } }] self.add_schedules(schedule_events) self.check_schedules_added(schedule_events) skip_jobs = [event['title'] for event in schedule_events] self.check_schedules_run(schedule_events, skip_jobs) def test_reload(self): start_date = datetime.datetime.strftime( datetime.datetime.utcnow() + datetime.timedelta(seconds=60), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'reload_1', 'title': 'date_job', 'actions': ['date_job_action'], 'config': { 'type': 'date', 'start_date': start_date } }, { 'id': 'reload_2', 'title': 'daily_job', 'actions': ['daily_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': start_date } }] self.add_schedules(schedule_events) self.check_schedules_added(schedule_events) self.check_schedules_run(schedule_events) self.test_engine.stop() del self.test_engine del self.test_client self.test_client = TestClient() self.test_engine = SchedulerEngine(self.test_client, 'test') for event in schedule_events: if 'last_run' in event: del event['last_run'] self.check_schedules_added(schedule_events) self.check_schedules_run(schedule_events, ('date_job', 'daily_job')) def test_delayed_load(self): self.test_engine.stop() del self.test_engine del self.test_client now = datetime.datetime.utcnow() if (now.second > 35): print('Sleep until the minute rolls over') time.sleep(60 - now.second) now = datetime.datetime.strftime(datetime.datetime.utcnow(), '%Y-%m-%dT%H:%M:%S.%fZ') self.schedule_events = [{ 'id': 'delay_1', 'title': 'date_job', 'actions': ['date_job_action'], 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'delay_2', 'title': 'daily_job', 'actions': ['daily_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': now } }, { 'id': 'delay_3', 'title': 'weekly_job', 'actions': ['weekly_job_action'], 'config': { 'type': 'interval', 'unit': 'week', 'interval': 1, 'start_date': now } }, { 'id': 'delay_4', 'title': 'monthly_job', 'actions': ['monthly_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': now } }, { 'id': 'delay_5', 'title': 'yearly_job', 'actions': ['yearly_job_action'], 'config': { 'type': 'interval', 'unit': 'year', 'interval': 1, 'start_date': now } }] for event in self.schedule_events: event_json = json.dumps(event) try: DbManager.Insert('scheduled_events', event['id'], event_json) except sqlite3.IntegrityError as e: DbManager.Update('scheduled_events', 'event = ?', event_json, 'id = ?', event['id']) print('Pause before loading scheduler') time.sleep(20) print('Starting scheduler, time is {}'.format( datetime.datetime.utcnow())) self.test_client = TestClient() self.test_engine = SchedulerEngine(self.test_client, 'test') self.check_schedules_run(self.schedule_events) def test_concurrent_updates(self): now = datetime.datetime.strftime(datetime.datetime.utcnow(), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'concurrent_1', 'title': 'date_job', 'actions': ['date_job_action'], 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'concurrent_1', 'title': 'date_job_updated', 'actions': ['date_job_action'], 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'concurrent_2', 'title': 'daily_job', 'actions': ['daily_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_2', 'title': 'daily_job_updated', 'actions': ['daily_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_3', 'title': 'weekly_job', 'actions': ['weekly_job_action'], 'config': { 'type': 'interval', 'unit': 'week', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_3', 'title': 'weekly_job_updated', 'actions': ['weekly_job_action'], 'config': { 'type': 'interval', 'unit': 'week', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_4', 'title': 'monthly_job', 'actions': ['monthly_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_4', 'title': 'monthly_job_updated', 'actions': ['monthly_job_action'], 'config': { 'type': 'interval', 'unit': 'month', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_5', 'title': 'yearly_job', 'actions': ['yearly_job_action'], 'config': { 'type': 'interval', 'unit': 'year', 'interval': 1, 'start_date': now } }, { 'id': 'concurrent_5', 'title': 'yearly_job_updated', 'actions': ['yearly_job_action'], 'config': { 'type': 'interval', 'unit': 'year', 'interval': 1, 'start_date': now } }] for event in schedule_events: threading.Thread(target=self.add_schedules, daemon=True, args=([event], )).start() #Only half the schedule_events should run since ones with the same id will overwrite previously added ones. Since we don't know what order that will take #we just make sure we only check that one of each action has run. run_events = { event['id']: event for event in schedule_events if 'id' in event } skip_jobs = [event['title'] for event in run_events.values()] self.check_schedules_run(schedule_events, skip_jobs) def test_update_schedules(self): start_date = datetime.datetime.strftime( datetime.datetime.utcnow() + datetime.timedelta(seconds=60), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'update_1', 'title': 'date_job', 'actions': ['date_job_action'], 'config': { 'type': 'date', 'start_date': start_date } }, { 'id': 'update_2', 'title': 'daily_job', 'actions': ['daily_job_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': start_date } }] self.add_schedules(schedule_events) update_schedule_events = [{ 'id': 'update_3', 'title': 'date_job_full_update', 'actions': ['date_job_full_update_action'], 'config': { 'type': 'date', 'start_date': start_date } }, { 'id': 'update_4', 'title': 'daily_job_full_update', 'actions': ['daily_job_full_update_action'], 'config': { 'type': 'interval', 'unit': 'day', 'interval': 1, 'start_date': start_date } }] self.assertTrue( self.test_engine.update_scheduled_events(update_schedule_events)) self.schedule_events = update_schedule_events self.check_schedules_run(update_schedule_events) def start_http_server(self): self.server = HTTPServer(('localhost', 8000), TestHandler) self.server.received = [] self.server.serve_forever() def test_http_notification(self): threading.Thread(target=self.start_http_server, daemon=True).start() now = datetime.datetime.strftime(datetime.datetime.utcnow(), '%Y-%m-%dT%H:%M:%S.%fZ') schedule_events = [{ 'id': 'http_1', 'title': 'date_get_job', 'actions': ['date_job_action'], 'http_push': { 'url': 'http://localhost:8000', 'method': 'GET', 'headers': { 'Content-Type': 'application/json' }, 'payload': { 'test': 'GET request' } }, 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'http_2', 'title': 'date_post_job', 'actions': ['date_job_action'], 'http_push': { 'url': 'http://localhost:8000', 'method': 'POST', 'headers': { 'Content-Type': 'application/json' }, 'payload': { 'test': 'POST request' } }, 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'http_3', 'title': 'date_put_job', 'actions': ['date_job_action'], 'http_push': { 'url': 'http://localhost:8000', 'method': 'PUT', 'headers': { 'Content-Type': 'application/json' }, 'payload': { 'test': 'PUT request' } }, 'config': { 'type': 'date', 'start_date': now } }, { 'id': 'http_4', 'title': 'date_delete_job', 'actions': ['date_job_action'], 'http_push': { 'url': 'http://localhost:8000', 'method': 'DELETE', 'headers': { 'Content-Type': 'application/json' }, 'payload': { 'test': 'DELETE request' } }, 'config': { 'type': 'date', 'start_date': now } }] self.add_schedules(schedule_events) self.check_schedules_added(schedule_events) self.check_schedules_run(schedule_events) self.assertEqual(4, len(self.server.received)) expected = [event['http_push']['payload'] for event in schedule_events] self.assertCountEqual(expected, self.server.received)
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 self.config = Config(APP_SETTINGS) self.networkConfig = Config(NETWORK_SETTINGS) 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.location = self.config.get('Agent', 'Location', "house0@room0@") self.mqtt_dis_prefix = self.config.get('Agent', 'MQTT_DIS_PREFIX', "homeassistant") self.connected = False self.exiting = Event() self.sensorsClient = None def __del__(self): """Delete the client""" self.Destroy() 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) if not self.username and not self.password and not self.clientId: self.CheckSubscription() if not self.Connect(): error('Error starting agent') return self.schedulerEngine = SchedulerEngine(self, 'client_scheduler') self.readQueue = Queue() self.writeQueue = Queue() self.hardware = Hardware() self.oSInfo = OSInfo() self.count = 10000 self.buff = bytearray(self.count) self.writerThread = WriterThread('writer', self) self.writerThread.start() self.processorThread = ProcessorThread('processor', self) self.processorThread.start() self.systemInfo = [] TimerThread(self.SendSystemInfo, 300) self.sensorsClient = sensors.SensorsClient(self) self.sensorsClient.SetDataChanged(self.OnDataChanged) # TimerThread(self.SendSystemState, 30, 5) self.updater = Updater(self.config) self.updater.start() events = self.schedulerEngine.get_scheduled_events() self.EnqueuePacket(events, cayennemqtt.JOBS_TOPIC) # 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)) def Destroy(self): """Destroy client and stop client threads""" info('Shutting down client') self.exiting.set() if hasattr(self, 'sensorsClient'): self.sensorsClient.StopMonitoring() if hasattr(self, 'schedulerEngine'): self.schedulerEngine.stop() if hasattr(self, 'updater'): self.updater.stop() if hasattr(self, 'writerThread'): self.writerThread.stop() if hasattr(self, 'processorThread'): self.processorThread.stop() ThreadPool.Shutdown() self.Disconnect() info('Client shut down') def OnDataChanged(self, data): """Enqueue a packet containing changed system data to send to the server""" try: if len(data) > 15: items = [ { item['channel']: item['value'] } for item in data if not item['channel'].startswith(cayennemqtt.SYS_GPIO) ] info('Send changed data: {} + {}'.format( items, cayennemqtt.SYS_GPIO)) else: info('Send changed data: {}'.format([{ item['channel']: item['value'] } for item in data])) # items = {} # gpio_items = {} # for item in data: # if not item['channel'].startswith(cayennemqtt.SYS_GPIO): # items[item['channel']] = item['value'] # else: # channel = item['channel'].replace(cayennemqtt.SYS_GPIO + ':', '').split(';') # if not channel[0] in gpio_items: # gpio_items[channel[0]] = str(item['value']) # else: # gpio_items[channel[0]] += ',' + str(item['value']) # info('Send changed data: {}, {}: {}'.format(items, cayennemqtt.SYS_GPIO, gpio_items)) except: info('Send changed data') pass self.EnqueuePacket(data) def SendSystemInfo(self): """Enqueue a packet containing system info to send to the server""" try: currentSystemInfo = [] cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_OS_NAME, value=self.oSInfo.ID) cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_OS_VERSION, value=self.oSInfo.VERSION_ID) cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.AGENT_VERSION, value=self.config.get( 'Agent', 'Version', __version__)) cayennemqtt.DataChannel.add(currentSystemInfo, cayennemqtt.SYS_POWER_RESET, value=0) cayennemqtt.DataChannel.add(currentSystemInfo, 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, 'OneWire': cayennemqtt.SYS_ONEWIRE, 'DeviceTree': cayennemqtt.SYS_DEVICETREE } for key, channel in channel_map.items(): try: cayennemqtt.DataChannel.add(currentSystemInfo, channel, value=config[key]) except: pass if currentSystemInfo != self.systemInfo: data = currentSystemInfo if self.systemInfo: data = [x for x in data if x not in self.systemInfo] if data: self.systemInfo = currentSystemInfo info('Send system info: {}'.format([{ item['channel']: item['value'] } for item in data])) self.EnqueuePacket(data) except Exception: exception('SendSystemInfo unexpected error') 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(self.config.path)) 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)) raise SystemExit else: info('Registration succeeded for invite code {}, credentials = {}'. format(inviteCode, credentials)) self.config.set('Agent', 'Initialized', 'true') try: 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 def Connect(self): """Connect to the server""" self.connected = False count = 0 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() self.connected = True except OSError as oserror: Daemon.OnFailure('cloud', oserror.errno) 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: 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.is_set(): debug('Restarting cycle...') sleep(1) self.Disconnect() self.Connect() def CheckJson(self, message): """Check if a JSON message is valid""" try: test = loads(message) except ValueError: return False return True def OnMessage(self, message): """Add message from the server to the queue""" info('OnMessage: {}'.format(message)) self.readQueue.put(message) def RunAction(self, action): """Run a specified action""" debug('RunAction: {}'.format(action)) result = True command = action.copy() self.mqttClient.transform_command(command) result = self.ExecuteMessage(command) return result def ProcessMessage(self): """Process a message from the server""" try: messageObject = self.readQueue.get(False) if not messageObject: return False except Empty: return False self.ExecuteMessage(messageObject) def ExecuteMessage(self, message): """Execute an action described in a message object Returns: True if action was executed, False otherwise.""" result = False if not message: return result channel = message['channel'] info('ExecuteMessage: {}'.format(message)) if channel in (cayennemqtt.SYS_POWER_RESET, cayennemqtt.SYS_POWER_HALT): result = self.ProcessPowerCommand(message) elif channel.startswith(cayennemqtt.DEV_SENSOR): result = self.ProcessSensorCommand(message) elif channel.startswith(cayennemqtt.SYS_GPIO): result = self.ProcessGpioCommand(message) elif channel == cayennemqtt.AGENT_DEVICES: result = self.ProcessDeviceCommand(message) elif channel in (cayennemqtt.SYS_I2C, cayennemqtt.SYS_SPI, cayennemqtt.SYS_UART, cayennemqtt.SYS_ONEWIRE): result = self.ProcessConfigCommand(message) elif channel == cayennemqtt.AGENT_MANAGE: result = self.ProcessAgentCommand(message) elif channel == cayennemqtt.AGENT_SCHEDULER: result = self.ProcessSchedulerCommand(message) else: info('Unknown message') return result def ProcessPowerCommand(self, message): """Process command to reboot/shutdown the system Returns: True if command was processed, False otherwise.""" error_message = None try: 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() info('Calling execute: {}'.format( commands[message['channel']])) 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_message = '{}: {}'.format(type(ex).__name__, ex) if error_message: error(error_message) data = [] cayennemqtt.DataChannel.add(data, message['channel'], value=0) self.EnqueuePacket(data) raise ExecuteMessageError(error_message) return error_message == None def ProcessAgentCommand(self, message): """Process command to manage the agent state Returns: True if command was processed, False otherwise.""" error = None try: if message['suffix'] == 'uninstall': 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' # 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) else: error = 'Unknown agent command: {}'.format(message['suffix']) except Exception as ex: error = '{}: {}'.format(type(ex).__name__, ex) self.EnqueueCommandResponse(message, error) if error: raise ExecuteMessageError(error) return error == None def ProcessConfigCommand(self, message): """Process system configuration command Returns: True if command was processed, False otherwise.""" 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_ONEWIRE: 19 } 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) return error == None def ProcessGpioCommand(self, message): """Process GPIO command Returns: True if command was processed, False otherwise.""" error = None try: channel = int(message['channel'].replace( cayennemqtt.SYS_GPIO + ':', '')) result = self.sensorsClient.GpioCommand( message.get('suffix', 'value'), 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) return error == None def ProcessSensorCommand(self, message): """Process sensor command Returns: True if command was processed, False otherwise.""" 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.get('suffix', 'value'), 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) return error == None def ProcessDeviceCommand(self, message): """Process a device command to add/edit/remove a sensor Returns: True if command was processed, False otherwise.""" error = None try: payload = message['payload'] info('ProcessDeviceCommand payload: {}'.format(payload)) if message['suffix'] == 'add': result = self.sensorsClient.AddSensor(payload['sensorId'], payload['description'], payload['class'], payload['args']) elif message['suffix'] == 'edit': result = self.sensorsClient.EditSensor(payload['sensorId'], payload['description'], payload['class'], payload['args']) elif message['suffix'] == 'delete': result = self.sensorsClient.RemoveSensor(payload['sensorId']) else: error = '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) return error == None def ProcessSchedulerCommand(self, message): """Process command to add/edit/remove a scheduled action Returns: True if command was processed, False otherwise.""" error = None try: if message['suffix'] == 'add': result = self.schedulerEngine.add_scheduled_event( message['payload'], True) elif message['suffix'] == 'edit': result = self.schedulerEngine.update_scheduled_event( message['payload']) elif message['suffix'] == 'delete': result = self.schedulerEngine.remove_scheduled_event( message['payload']) elif message['suffix'] == 'get': events = self.schedulerEngine.get_scheduled_events() self.EnqueuePacket(events, cayennemqtt.JOBS_TOPIC) else: error = 'Unknown schedule command: {}'.format( message['suffix']) debug('ProcessSchedulerCommand result: {}'.format(result)) if result is False: error = 'Schedule command failed' except Exception as ex: error = '{}: {}'.format(type(ex).__name__, ex) self.EnqueueCommandResponse(message, error) return error == None def EnqueueCommandResponse(self, message, error): """Send response after processing a command message""" if not 'cmdId' in message: # If there is no command id we assume this is a scheduled command and don't send a response message. return debug('EnqueueCommandResponse error: {}'.format(error)) if error: response = 'error,{}={}'.format(message['cmdId'], error) else: response = 'ok,{}'.format(message['cmdId']) info(response) self.EnqueuePacket(response, cayennemqtt.COMMAND_RESPONSE_TOPIC) def EnqueuePacket(self, message, topic=cayennemqtt.DATA_TOPIC): """Enqueue a message packet to send to the server""" packet = (topic, message) self.writeQueue.put(packet) def DequeuePacket(self): """Dequeue a message packet to send to the server""" packet = (None, None) try: packet = self.writeQueue.get(False) except Empty: pass return packet