Esempio n. 1
0
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)
Esempio n. 2
0
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