Example #1
0
    def __init__(self, config):
        frameSize = (config.gui.windowWidth,
                     config.gui.windowHeight)
        super().__init__(None, title="", size=frameSize)

        self.__config = config

        # The central controller requires a secret authorisation key, this is
        # sent as part of the request header and is defined as part of the
        # configuration file.
        self.__authorisationKey = self.__config.centralController.authKey

        self.__panel = wx.Panel(self)

        endpoint = self.__config.centralController.endpoint
        self.__APIClient = APIEndpointClient(endpoint)

        # Key sequence pressed.
        self.__keySequence = ''

        # Create sequence timer object and bind the timeout event.
        self.__sequenceTimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.__TimeoutEvent, self.__sequenceTimer)

        self.__CreateUserInterface()

        # make sure reactor.stop() is used to stop event loop
        self.Bind(wx.EVT_CLOSE, self.__OnExit)
Example #2
0
    def __init__(self, controllerDb, config, eventMgr, logger):
        self._config = config
        self._current_alarm_state = self.AlarmState.Deactivated
        self._database = controllerDb
        self._event_mgr = eventMgr
        self._failed_entry_attempts = 0
        self._logger = logger
        self._transient_states = []
        self._unable_to_conn_error_displayed = False

        endpoint = self._config.keypad_controller.endpoint
        self._keypad_api_client = APIEndpointClient(endpoint)
    def __init__(self, config, logger):
        self.__config = config
        self._current_panel = (None, None)
        self._new_panel = (self.PanelType.CommunicationsLost, {})
        self._keypad_code = ''
        self._logger = logger

        self._comms_lost_panel = CommsLostPanel(self.__config)
        self._keypad_locked_panel = LockedPanel(self.__config)
        self._keypad_panel = KeypadPanel(self.__config)

        self._last_reconnect_time = 0

        endpoint = self.__config.centralController.endpoint
        self._central_ctrl_api_client = APIEndpointClient(endpoint)
    def __init__(self, parent, config):
        wx.Panel.__init__(self, parent)

        self._config = config
        self._api_client = APIEndpointClient(config.centralController.endpoint)
        self._logs = []
        self._logs_last_msg_timestamp = 0
        self._last_log_id = 0

        top_splitter = wx.SplitterWindow(self)
        self._config_panel = CentralControllerConfigPanel(top_splitter)
        self._logs_panel = ConsoleLogsPanel(top_splitter)
        top_splitter.SplitHorizontally(self._config_panel, self._logs_panel)
        top_splitter.SetSashGravity(0.5)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(top_splitter, 1, wx.EXPAND)
        self.SetSizer(sizer)
Example #5
0
class KeypadPanel(wx.Frame):

    ## Sequence timeout in seconds.
    SequenceTimeout = 5


    ## KeypadPanel class constructor.
    #  @param self The object pointer.
    #  @param config Configuration items.
    def __init__(self, config):
        frameSize = (config.gui.windowWidth,
                     config.gui.windowHeight)
        super().__init__(None, title="", size=frameSize)

        self.__config = config

        # The central controller requires a secret authorisation key, this is
        # sent as part of the request header and is defined as part of the
        # configuration file.
        self.__authorisationKey = self.__config.centralController.authKey

        self.__panel = wx.Panel(self)

        endpoint = self.__config.centralController.endpoint
        self.__APIClient = APIEndpointClient(endpoint)

        # Key sequence pressed.
        self.__keySequence = ''

        # Create sequence timer object and bind the timeout event.
        self.__sequenceTimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.__TimeoutEvent, self.__sequenceTimer)

        self.__CreateUserInterface()

        # make sure reactor.stop() is used to stop event loop
        self.Bind(wx.EVT_CLOSE, self.__OnExit)


    def Display(self):
        self.Show()
        if self.__config.gui.fullscreen:
            self.ShowFullScreen(True)
            self.Maximize(True)


    ## Create the keypad user interface.
    #  @param self The object pointer.
    def __CreateUserInterface(self):
        # Sizer that all of the buttons will be place into.
        mainSizer = wx.BoxSizer(wx.VERTICAL)

        #font = wx.Font(12, wx.MODERN, wx.NORMAL, wx.NORMAL)

        self.__buttonsList = {}
        self.__defaultButtonDetails = {}

        # Array with the order and labels for the buttons.  There are two
        # special buttons:
        # * Go - enters the passcode that has typed in.
        # * Reset - Resets the sequence entered.
        buttons = [['7', '8', '9'],
                   ['4', '5', '6'],
                   ['1', '2', '3'],
                   ['0', 'GO', 'Reset']]

        for labelList in buttons:
            btnSizer = wx.BoxSizer()
            for label in labelList:
                button = wx.Button(self.__panel, label=label)
                btnSizer.Add(button, 1, wx.EXPAND, 0)
                self.__buttonsList[button] = button

                self.__defaultButtonDetails[button] = \
                {
                    'backgroundColour' : button.GetBackgroundColour(),
                    'label' : label
                }

                if label == 'GO':
                    button.Bind(wx.EVT_BUTTON, self.__TryTransmittingKeyCode)

                elif label == 'Reset':
                    button.Bind(wx.EVT_BUTTON, self.__ResetKeypad)

                else:
                    button.Bind(wx.EVT_BUTTON, self.__PressKey)

            mainSizer.Add(btnSizer, 1, wx.EXPAND)

        self.__panel.SetSizer(mainSizer)


    ## A key is pressed event handler.  If this is the 1st key in the sequence
    #  then start a timer which is your allotted time to enter the right values
    #  before they are cleared.  All of the keypresses are stored internally,
    #  ready for transmission.
    #  @param self The object pointer.
    #  @param event Key pressed event object.
    def __PressKey(self, event):

        # Get the event object for the key pressed.
        pressedKey = event.GetEventObject()
        pressedKeyValue = pressedKey.GetLabel()

        if not self.__keySequence:
            self.__sequenceTimer.Start(self.SequenceTimeout * 1000)

        self.__keySequence = self.__keySequence + pressedKeyValue


    ## Reset the keypad, which involves clearing key sequence.
     #  @param self The object pointer.
     #  @param event Unused.
    def __ResetKeypad(self, event=None):
        # pylint: disable=W0613

        self.__keySequence = ''


    ## Try to transmit the entered key sequence to the alarm master controller.
    #  The code will only be transmitted if 1 or more keys were pressed, also
    #  on transmission the sequence timer is stopped and sequence reset.
    #  @param self The object pointer.
    #  @param event Unused.
    def __TryTransmittingKeyCode(self, event):
        # pylint: disable=W0613

        additionalHeaders = {
            'authorisationKey' : self.__authorisationKey
        }

        if not self.__keySequence:
            return

        keySeq = self.__keySequence

        body = {"keySequence": self.__keySequence}
        jsonBody = json.dumps(body)

        self.__TimeoutEvent()

        response = self.__APIClient.SendPostMsg('receiveKeyCode',
                                                MIMEType.JSON,
                                                additionalHeaders,
                                                jsonBody)

        if response is None:
            print(f'failed to transmit, reason : {self.__APIClient.LastErrMsg}')
            return

        # 400 Bad Request : Missing or invalid json body or validation failed.
        if response.status_code == HTTPStatusCode.BadRequest:
            # TODO : Add a log message here
            print('failed to transmit, reason : BadRequest')
            return

        # 401 Unauthenticated : Missing or invalid authentication key.
        if response.status_code == HTTPStatusCode.Unauthenticated:
            # TODO : Add a log message here
            print('failed to transmit, reason : Unauthenticated')
            return

        # 200 OK : code accepted, code incorrect or code refused.
        if response.status_code == HTTPStatusCode.OK:
            return


    ## Timer timeout event function.  This will cause any stored key sequence
    #  to be cleared and the timer stopped, ready for when the next key is
    #  pressed.
    #  @param self The object pointer.
    #  @param event Unused.
    def __TimeoutEvent(self, event=None):
        # pylint: disable=W0613

        self.__ResetKeypad()
        self.__sequenceTimer.Stop()


    ## Exit event function when the application is closed.
    #  @param self The object pointer.
    #  @param event Unused, but required.
    def __OnExit(self, event):
        # pylint: disable=R0201
        # pylint: disable=W0613
        reactor.stop()
Example #6
0
class StateManager:
    # pylint: disable=too-many-instance-attributes

    __slots__ = [
        '_config', '_current_alarm_state', '_database', '_event_mgr',
        '_failed_entry_attempts', '_keypad_api_client', '_logger',
        '_transient_states', '_unable_to_conn_error_displayed'
    ]

    ## Alarm state enumeration.
    class AlarmState(enum.Enum):
        ## Alarm state : alarm is deactivated.
        Deactivated = 0

        ## Alarm state : alarm is activated.
        Activated = 1

        Triggered = 2

    ## StateManager class default constructor.
    #  @param self The object pointer.
    #  @param controllerDb Database controller interface instance.
    #  @param config Configuration items in json format.
    #  @param eventMgr Event manager instance.
    def __init__(self, controllerDb, config, eventMgr, logger):
        self._config = config
        self._current_alarm_state = self.AlarmState.Deactivated
        self._database = controllerDb
        self._event_mgr = eventMgr
        self._failed_entry_attempts = 0
        self._logger = logger
        self._transient_states = []
        self._unable_to_conn_error_displayed = False

        endpoint = self._config.keypad_controller.endpoint
        self._keypad_api_client = APIEndpointClient(endpoint)

    ## Received events from the keypad.
    #  @param self The object pointer.
    #  @param eventInst Receieved keypad event.
    def rcv_keypad_event(self, event):
        if event.id == Evts.EvtType.KeypadKeyCodeEntered:
            self._handle_key_code_entered_event(event)

    #  @param self The object pointer.
    def rcv_device_event(self, event):
        if event.id == Evts.EvtType.SensorDeviceStateChange:
            self._handle_sensor_device_state_change_event(event)

    ## Attemp to send an 'Alive Ping' message to the keypad, this is done when
    ## the keypad needs waking up after a sytem boot.
    #  @param self The object pointer.
    #  @param eventInst Event class that was created to raise this event.
    def send_alive_ping_msg(self, event):

        additional_headers = {
            'authorisationKey': self._config.keypad_controller.authKey
        }

        response = self._keypad_api_client.SendPostMsg(
            'receiveCentralControllerPing', MIMEType.JSON, additional_headers,
            {})

        if response is None:
            if not self._unable_to_conn_error_displayed:
                msg = f'Unable to communicate with keypad, reason : ' +\
                    f'{self._keypad_api_client.LastErrMsg}'
                self._logger.Log(LogType.Info, msg)
                self._event_mgr.QueueEvent(event)
                self._unable_to_conn_error_displayed = True
            return

        # 401 Unauthenticated : Missing authentication key.
        if response.status_code == HTTPStatusCode.Unauthenticated:
            self._logger.Log(LogType.Critical,
                             'Keypad cannot send AlivePing as the ' +\
                             'authorisation key is missing')
            return

        # 403 forbidden : Invalid authentication key.
        if response.status_code == HTTPStatusCode.Forbidden:
            self._logger.Log(LogType.Critical,
                             'Keypad cannot send AlivePing as the ' +\
                             'authorisation key is incorrect')
            return

        # 200 OK : code accepted, code incorrect or code refused.
        if response.status_code == HTTPStatusCode.OK:
            msg = f"Successfully send 'AlivePing' to keypad controller"
            self._logger.Log(LogType.Info, msg)

        self._unable_to_conn_error_displayed = False

    def send_keypad_locked_msg(self, event):
        additional_headers = {
            'authorisationKey': self._config.keypad_controller.authKey
        }
        json_body = json.dumps(event.body)
        response = self._keypad_api_client.SendPostMsg('receiveKeypadLock',
                                                       MIMEType.JSON,
                                                       additional_headers,
                                                       json_body)

        if response is None:
            msg = f'Keypad locked msg : Unable to communicate with keypad, ' +\
                  f'reason : {self._keypad_api_client.LastErrMsg}'
            self._logger.Log(LogType.Debug, msg)
            self._event_mgr.QueueEvent(event)
            return

        # 401 Unauthenticated : Missing authentication key.
        if response.status_code == HTTPStatusCode.Unauthenticated:
            self._logger.Log(LogType.Critical,
                             'Keypad locked msg : Cannot send the ' +\
                             'AlivePing as the authorisation key ' +\
                             'is missing')
            return

        # 403 forbidden : Invalid authentication key.
        if response.status_code == HTTPStatusCode.Forbidden:
            self._logger.Log(LogType.Critical,
                             'Keypad locked msg : Authorisation ' +\
                             'key is incorrect')
            return

        # 200 OK : code accepted, code incorrect or code refused.
        if response.status_code == HTTPStatusCode.OK:
            msg = "Successfully sent 'Keypad locked msg' to keypad controller"
            self._logger.Log(LogType.Debug, msg)

    #  @param self The object pointer.
    def update_transitory_events(self):

        # List of event id's that need to be removed
        id_list = []

        ## Transitory events go here....

        # Final stage is to remove all any of the transactions that have been
        # marked for removal.
        if id_list:
            self._transient_states = [evt for evt in \
                self._transient_states if evt.id not in id_list]

    ## Function to handle a a keycode has been entered.
    #  @param self The object pointer.
    #  @param eventInst The event that contains a keycode.
    def _handle_key_code_entered_event(self, event):
        body = event.body

        key_sequence = body[schemas.ReceiveKeyCode.BodyElement.KeySeq]

        # Read the key code detail from the database.
        details = self._database.get_keycode_details(key_sequence)

        if details is not None:
            if self._current_alarm_state == self.AlarmState.Triggered:
                self._logger.Log(LogType.Info,
                                 'A triggered alarm has been deactivated')
                self._current_alarm_state = self.AlarmState.Deactivated
                self._failed_entry_attempts = 0
                evt = Event(Evts.EvtType.DeactivateSiren, None)
                self._event_mgr.QueueEvent(evt)
                self._deactivate_alarm()

            elif self._current_alarm_state == self.AlarmState.Deactivated:
                self._logger.Log(LogType.Info, 'The alarm has been activated')
                self._current_alarm_state = self.AlarmState.Activated
                self._failed_entry_attempts = 0
                self._trigger_alarm()

            elif self._current_alarm_state == self.AlarmState.Activated:
                self._logger.Log(LogType.Info,
                                 'The alarm has been deactivated')
                self._current_alarm_state = self.AlarmState.Deactivated
                self._failed_entry_attempts = 0
                self._deactivate_alarm()

        else:
            self._logger.Log(LogType.Info,
                             'An invalid key code was entered on keypad')
            self._failed_entry_attempts += 1

            attempts = self._failed_entry_attempts

            # If the attempt failed then send the response of type
            # receiveKeyCodeResponseAction_KeycodeIncorrect along with any
            # response actions that have been defined in the configuraution
            # file.
            if attempts in self._config.failed_attempt_responses:
                responses = self._config.failed_attempt_responses[attempts]

                for response in responses:

                    if response == 'disableKeyPad':
                        lock_event_body = {
                            keypadApi.KeypadLockRequest.BodyElement.LockTime:
                            round(time.time()) +
                            int(responses[response]['lockTime'])
                        }
                        lock_event = Event(
                            Evts.EvtType.KeypadApiSendKeypadLock,
                            lock_event_body)
                        self._event_mgr.QueueEvent(lock_event)

                    elif response == 'triggerAlarm':
                        if self._current_alarm_state != self.AlarmState.Triggered:
                            self._logger.Log(LogType.Info,
                                             '|=> Alarm has been triggered!')
                            self._trigger_alarm(no_grace_time=True)

                    elif response == 'resetAttemptAccount':
                        self._failed_entry_attempts = 0

    ## Function to handle the alarm being triggered.
    #  @param self The object pointer.
    def _trigger_alarm(self, no_grace_time=False):
        self._current_alarm_state = self.AlarmState.Activated

        alarm_set_evt_body = {
            'activationTimestamp': time.time(),
            'noGraceTime': no_grace_time
        }

        activate_event = Event(Evts.EvtType.AlarmActivated, alarm_set_evt_body)
        self._event_mgr.QueueEvent(activate_event)

    ## Function to handle the alarm being deactivated.
    #  @param self The object pointer.
    def _deactivate_alarm(self):
        self._current_alarm_state = self.AlarmState.Deactivated
        self._failed_entry_attempts = 0

        evt = Event(Evts.EvtType.AlarmDeactivated)
        self._event_mgr.QueueEvent(evt)

    ## Event handler for a sensor device state change.
    #  @param self The object pointer.
    #  @param eventInst Device change event.
    def _handle_sensor_device_state_change_event(self, event):
        body = event.body
        device_name = body[Evts.SensorDeviceBodyItem.DeviceName]
        state = body[Evts.SensorDeviceBodyItem.State]

        triggered = bool(state == 1)
        state_str = "opened" if triggered else "closed"

        # If the alarm is deactived then ignore the sensor state change after
        # logging the change for reference.
        if self._current_alarm_state == self.AlarmState.Deactivated:
            log_msg = f"{device_name} was {state_str}, although alarm isn't on"
            self._logger.Log(LogType.Info, log_msg)
            return

        # If the trigger has has already been triggered then opening or closing
        # a door etc. would change the alarm state, although we should log that
        # the even occurred.
        if self._current_alarm_state == self.AlarmState.Triggered:
            log_msg = f"{device_name} was {state_str}, alarm already triggered"
            self._logger.Log(LogType.Info, log_msg)
            return

        if self._current_alarm_state == self.AlarmState.Activated:
            log_msg = f"Activity on {device_name} ({state_str}) has triggerd " +\
                "the alarm!"
            self._logger.Log(LogType.Info, log_msg)
            self._current_alarm_state = self.AlarmState.Triggered

            evt = Event(Evts.EvtType.ActivateSiren, None)
            self._event_mgr.QueueEvent(evt)
class CentralControllerPanel(wx.Panel):
    # pylint: disable=too-few-public-methods

    RetrieveConsoleLogsPath = '/retrieveConsoleLogs'

    def __init__(self, parent, config):
        wx.Panel.__init__(self, parent)

        self._config = config
        self._api_client = APIEndpointClient(config.centralController.endpoint)
        self._logs = []
        self._logs_last_msg_timestamp = 0
        self._last_log_id = 0

        top_splitter = wx.SplitterWindow(self)
        self._config_panel = CentralControllerConfigPanel(top_splitter)
        self._logs_panel = ConsoleLogsPanel(top_splitter)
        top_splitter.SplitHorizontally(self._config_panel, self._logs_panel)
        top_splitter.SetSashGravity(0.5)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(top_splitter, 1, wx.EXPAND)
        self.SetSizer(sizer)

    def get_logs(self):

        msg_body = {"startTimestamp": self._logs_last_msg_timestamp}

        additional_headers = {
            'authorisationKey': self._config.centralController.authKey
        }

        response = self._api_client.SendPostMsg(self.RetrieveConsoleLogsPath,
                                                MIMEType.JSON,
                                                additional_headers,
                                                json.dumps(msg_body))

        # Not able to communicated with the central controller.
        if response is None:
            # NOT able to communicate with central controller...
            return

        if response.status_code != HTTPStatusCode.OK:
            print("Communications error with central controller, " + \
                  f"status {response.status_code}")
            print(response.text)
            return

        msg_body = response.json()

        # Validate that the json body conforms to the expected schema.
        # If the message isn't valid then a 400 error should be generated.
        try:
            jsonschema.validate(instance=msg_body,
                                schema=schemas.RequestLogsResponse.Schema)

        # Caught a message body validation failed, abort read.
        except jsonschema.exceptions.ValidationError:
            return

        self._update_log_entries(msg_body)

    def _update_log_entries(self, msg_body):
        body_elements = schemas.RequestLogsResponse.BodyElement

        last_msg_timestamp = msg_body[body_elements.LastTimestamp]

        # If the last message timestamp is 0 then we have no new log messages.
        if last_msg_timestamp == 0:
            return

        self._logs_last_msg_timestamp = last_msg_timestamp

        for entry in msg_body[body_elements.Entries]:
            timestamp = entry[body_elements.EntryTimestamp]
            timestamp_str = datetime.fromtimestamp(timestamp).strftime(
                '%Y-%m-%d %H:%M:%S')

            msg = f"{timestamp_str} {entry[body_elements.EntryMessage]}"

            self._logs_panel.add_log_entry(self._last_log_id,
                                           entry[body_elements.EntryMsgLevel],
                                           msg)
            self._last_log_id += 1
class KeypadStateObject:
    # pylint: disable=too-many-instance-attributes

    __slots__ = [
        '_central_ctrl_api_client', '_comms_lost_panel', '__config',
        '_current_panel', '_keypad_code', '_keypad_locked_panel',
        '_keypad_panel', '_last_reconnect_time', '_logger', '_new_panel'
    ]

    CommLostRetryInterval = 5

    class PanelType(enum.Enum):
        KeypadIsLocked = 0
        CommunicationsLost = 1
        Keypad = 2

    @property
    def new_panel(self):
        return self._new_panel

    @new_panel.setter
    def new_panel(self, new_panel_type):
        self._new_panel = new_panel_type

    @property
    def current_panel(self):
        return self._current_panel

    def __init__(self, config, logger):
        self.__config = config
        self._current_panel = (None, None)
        self._new_panel = (self.PanelType.CommunicationsLost, {})
        self._keypad_code = ''
        self._logger = logger

        self._comms_lost_panel = CommsLostPanel(self.__config)
        self._keypad_locked_panel = LockedPanel(self.__config)
        self._keypad_panel = KeypadPanel(self.__config)

        self._last_reconnect_time = 0

        endpoint = self.__config.centralController.endpoint
        self._central_ctrl_api_client = APIEndpointClient(endpoint)

    ## Function that is called to check if the panel has changed or needs to
    ## be changed (e.g. keypad lock expired).
    #  @param self The object pointer.
    def check_panel(self):
        if self._current_panel[0] != self._new_panel[0]:
            self._current_panel = self._new_panel
            self._update_displayed_panel()
            return

        # If the keypad is currently locked then we need to check to see if
        # the keypad lock has timed out, if it has then reset the panel.
        if self._current_panel[
                0] == KeypadStateObject.PanelType.KeypadIsLocked:
            curr_time = time.time()

            if curr_time >= self._current_panel[1]:
                keypad_panel = (KeypadStateObject.PanelType.Keypad, {})
                self._current_panel = keypad_panel
                self._update_displayed_panel()

            return

        # If the current panel is 'communications lost' then try to send a
        # please respond message to the central controller only at the alotted
        # intervals.
        if self._current_panel[
                0] == KeypadStateObject.PanelType.CommunicationsLost:
            curr_time = time.time()

            if curr_time > self._last_reconnect_time + self.CommLostRetryInterval:
                self._last_reconnect_time = curr_time
                reactor.callFromThread(self._send_please_respond_msg)

    #  @param self The object pointer.
    def _send_please_respond_msg(self):

        additional_headers = {
            'authorisationKey': self.__config.centralController.authKey
        }

        response = self._central_ctrl_api_client.SendPostMsg(
            'pleaseRespondToKeypad', MIMEType.JSON, additional_headers)

        if response is None:
            self._logger.Log(LogType.Warn, 'failed to transmit, reason : %s',
                             self._central_ctrl_api_client.LastErrMsg)
            return

        # 400 Bad Request : Missing or invalid json body or validation failed.
        if response.status_code == HTTPStatusCode.BadRequest:
            self._logger.Log(LogType.Warn,
                             'failed to transmit, reason : BadRequest')
            return

        # 401 Unauthenticated : Missing or invalid authentication key.
        if response.status_code == HTTPStatusCode.Unauthenticated:
            self._logger.Log(LogType.Warn,
                             'failed to transmit, reason : Unauthenticated')
            return

        # 200 OK : code accepted, code incorrect or code refused.
        if response.status_code == HTTPStatusCode.OK:
            return

    ## Display a new panel by firstly hiding all of panels and then after that
    ## show just the expected one.
    #  @param self The object pointer.
    def _update_displayed_panel(self):
        self._comms_lost_panel.Hide()
        self._keypad_panel.Hide()
        self._keypad_locked_panel.Hide()

        panel, _ = self._current_panel

        if panel == KeypadStateObject.PanelType.KeypadIsLocked:
            self._keypad_locked_panel.Display()

        elif panel == KeypadStateObject.PanelType.CommunicationsLost:
            self._comms_lost_panel.Display()

        elif panel == KeypadStateObject.PanelType.Keypad:
            self._keypad_panel.Display()

        # The displayed panel has changed, we can now reset newPanel.
        self._new_panel = self._current_panel
class KeypadControllerPanel(wx.Panel):
    # pylint: disable=too-few-public-methods
    # pylint: disable=too-many-instance-attributes

    RetrieveConsoleLogsPath = '/retrieveConsoleLogs'
    HealthStatusPath = '/_healthStatus'

    def __init__(self, parent, config):
        wx.Panel.__init__(self, parent)

        self._config = config
        self._api_client = APIEndpointClient(config.keypadController.endpoint)
        self._logs = []
        self._logs_last_msg_timestamp = 0
        self._last_log_id = 0
        self._main_window = parent
        self._status = (False, '')

        top_splitter = wx.SplitterWindow(self)
        self._config_panel = KeypadControllerConfigPanel(top_splitter)
        self._logs_panel = ConsoleLogsPanel(top_splitter)
        top_splitter.SplitHorizontally(self._config_panel, self._logs_panel)
        top_splitter.SetSashGravity(0.5)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(top_splitter, 1, wx.EXPAND)
        self.SetSizer(sizer)

    def get_logs(self):

        if not self._check_connection_status():
            return

        msg_body = {"startTimestamp": self._logs_last_msg_timestamp}

        additional_headers = {
            'authorisationKey': self._config.keypadController.authKey
        }

        response = self._api_client.SendPostMsg(self.RetrieveConsoleLogsPath,
                                                MIMEType.JSON,
                                                additional_headers,
                                                json.dumps(msg_body))

        # Not able to communicated with the central controller.
        if response is None:
            # NOT able to communicate with central controller...
            return

        if response.status_code != HTTPStatusCode.OK:
            print("Communications error with central controller, " + \
                  f"status {response.status_code}")
            print(response.text)
            return

        msg_body = response.json()

        # Validate that the json body conforms to the expected schema.
        # If the message isn't valid then a 400 error should be generated.
        try:
            jsonschema.validate(instance=msg_body,
                                schema=schemas.RequestLogsResponse.Schema)

        # Caught a message body validation failed, abort read.
        except jsonschema.exceptions.ValidationError:
            return

        self._update_log_entries(msg_body)

    def _update_log_entries(self, msg_body):
        body_elements = schemas.RequestLogsResponse.BodyElement

        last_msg_timestamp = msg_body[body_elements.LastTimestamp]

        # If the last message timestamp is 0 then we have no new log messages.
        if last_msg_timestamp == 0:
            return

        self._logs_last_msg_timestamp = last_msg_timestamp

        for entry in msg_body[body_elements.Entries]:
            timestamp = entry[body_elements.EntryTimestamp]
            timestamp_str = datetime.fromtimestamp(timestamp).strftime(
                '%Y-%m-%d %H:%M:%S')

            msg = f"{timestamp_str} {entry[body_elements.EntryMessage]}"

            self._logs_panel.add_log_entry(self._last_log_id,
                                           entry[body_elements.EntryMsgLevel],
                                           msg)
            self._last_log_id += 1

    def _check_connection_status(self):

        additional_headers = {
            'authorisationKey': self._config.keypadController.authKey
        }

        response = self._api_client.SendGetMsg(self.HealthStatusPath,
                                               MIMEType.JSON,
                                               additional_headers)

        # We are not able to communicate with the keypad controller...
        if response is None:
            self._update_status((False, 'No connection'))
            return False

        if response.status_code != HTTPStatusCode.OK:
            self._update_status((False, 'API Error'))
            return False

        # Initially hard-code to normal as it's hard-coded into the response.
        self._update_status((True, 'Normal'))
        return True

    def _update_status(self, new_status):
        curr_state, curr_str = self._status
        new_state, new_str = new_status

        # If no change at all we can leave now...
        if curr_state == new_state and curr_str == new_str:
            return

        if new_state:
            new_str = f'Connected: ({new_str})'
            self._main_window.update_keypad_status(new_str)

        else:
            new_str = f'Disconnected: ({new_str})'
            self._main_window.update_keypad_status(new_str)

        self._status = new_status