def test_send_message_none_parameter_raises_value_error(self):
     """ Test passes when send_message_to_device() raises ValueError
     """
     self.user = FlowUser(username, password)
     device = self.user.find_device(device_type)
     with self.assertRaises(ValueError):
         self.user.send_message_to_device(device, "", 20)
Exemple #2
0
    def initialize(self, user_name, password):
        """ Initializes the system

        Initializes flow libraries and connects to server
        Logs in as user and tries to find the controller device, caches if found
        :param str user_name: flow user name
        :param str password: flow user password
        :raises ControllerNotFound, LoginFailed, FlowLibraryError
        """
        flow_url, flow_key, flow_secret = get_server_details()
        try:
            flow_initialize(flow_url, flow_key, flow_secret)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise FlowLibraryError("Failed to initialize flow library")

        try:
            self.__flow_user = FlowUser(user_name, password)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise LoginFailed()
        except ValueError as value_error:
            LOGGER.exception(value_error)
            raise LoginFailed()

        try:
            self.__controller_device = self.__flow_user.find_device(
                CONTROLLER_DEVICE_TYPE)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise ControllerNotFound()

        # enable message reception
        self.__flow_user.enable_message_reception(
            ClimateControlModel.message_received_callback)
Exemple #3
0
class TestFlowDevice(unittest.TestCase):
    """ Testing of FlowDevice class members
    """
    def setUp(self):
        flow_initialize(flow_server_url, flow_server_key, flow_server_secret)
        self.user = FlowUser(username, password)
        self.device = self.user.find_device(device_type)

    def test_get_device_type_expected(self):
        """ Test to check get_device_type()
        """
        self.assertEqual(self.device.get_device_type(), device_type)

    def test_get_device_name_expected(self):
        """ Test to check get_device_name()
        """
        self.assertIsNotNone(self.device.get_device_name())

    def test_get_device_id_expected(self):
        """ Test to check get_device_id()
        """
        self.assertIsNotNone(self.device.get_device_id())

    def test_save_setting(self):
        """ Test to check save_settings()
        """
        self.device.save_setting(SETTING_KEY, SETTING_VALUE)

    def test_save_setting_none_parameters_raises_value_error(self):
        """ Test passes when save_settings() raises ValueError
        """
        with self.assertRaises(ValueError):
            self.device.save_setting("", "")

    def test_get_setting_expected(self):
        """ Test to check get_settings()
        """
        self.device.save_setting(SETTING_KEY, SETTING_VALUE)
        self.assertEqual(self.device.get_setting(SETTING_KEY), SETTING_VALUE)

    def test_get_setting_wrong_parameter_raises_flow_exception(self):
        """ Test passes when get_settings() raises FlowCoreException
        """
        with self.assertRaises(FlowCoreException):
            self.device.get_setting("unknown_key")

    def test_get_setting_none_parameter_raises_value_error(self):
        """ Test passes when get_setting() raises ValueError
        """
        with self.assertRaises(ValueError):
            self.device.get_setting("")
class TestFlowDevice(unittest.TestCase):
    """ Testing of FlowDevice class members
    """
    def setUp(self):
        flow_initialize(flow_server_url, flow_server_key, flow_server_secret)
        self.user = FlowUser(username, password)
        self.device = self.user.find_device(device_type)

    def test_get_device_type_expected(self):
        """ Test to check get_device_type()
        """
        self.assertEqual(self.device.get_device_type(), device_type)

    def test_get_device_name_expected(self):
        """ Test to check get_device_name()
        """
        self.assertIsNotNone(self.device.get_device_name())

    def test_get_device_id_expected(self):
        """ Test to check get_device_id()
        """
        self.assertIsNotNone(self.device.get_device_id())

    def test_save_setting(self):
        """ Test to check save_settings()
        """
        self.device.save_setting(SETTING_KEY, SETTING_VALUE)

    def test_save_setting_none_parameters_raises_value_error(self):
        """ Test passes when save_settings() raises ValueError
        """
        with self.assertRaises(ValueError):
            self.device.save_setting("", "")

    def test_get_setting_expected(self):
        """ Test to check get_settings()
        """
        self.device.save_setting(SETTING_KEY, SETTING_VALUE)
        self.assertEqual(self.device.get_setting(SETTING_KEY), SETTING_VALUE)

    def test_get_setting_wrong_parameter_raises_flow_exception(self):
        """ Test passes when get_settings() raises FlowCoreException
        """
        with self.assertRaises(FlowCoreException):
            self.device.get_setting("unknown_key")

    def test_get_setting_none_parameter_raises_value_error(self):
        """ Test passes when get_setting() raises ValueError
        """
        with self.assertRaises(ValueError):
            self.device.get_setting("")
    def initialize(self, user_name, password):
        """ Initializes the system

        Initializes flow libraries and connects to server
        Logs in as user and tries to find the controller device, caches if found
        :param str user_name: flow user name
        :param str password: flow user password
        :raises ControllerNotFound, LoginFailed, FlowLibraryError
        """
        flow_url, flow_key, flow_secret = get_server_details()
        try:
            flow_initialize(flow_url, flow_key, flow_secret)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise FlowLibraryError("Failed to initialize flow library")

        try:
            self.__flow_user = FlowUser(user_name, password)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise LoginFailed()
        except ValueError as value_error:
            LOGGER.exception(value_error)
            raise LoginFailed()

        try:
            self.__controller_device = self.__flow_user.find_device(CONTROLLER_DEVICE_TYPE)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise ControllerNotFound()

        # enable message reception
        self.__flow_user.enable_message_reception(ClimateControlModel.message_received_callback)
 def test_find_device_wrong_parameter_raises_flow_exception(self):
     """Test passes when no device of particular type found
     """
     self.user = FlowUser(username, password)
     with self.assertRaises(FlowException):
         self.user.find_device("unknown_device")
Exemple #7
0
 def setUp(self):
     flow_initialize(flow_server_url, flow_server_key, flow_server_secret)
     self.user = FlowUser(username, password)
     self.device = self.user.find_device(device_type)
Exemple #8
0
class ClimateControlModel(QObject):
    """ Model class representing the climate control system

    Talks with flow, fetches required data from flow, caches some of the data
    """
    __message_received_signal = None
    setting_received = Signal(dict)
    save_setting_result = Signal(dict)
    measurement_changed = Signal(MeasurementEvent)
    relay_status_changed = Signal(RelayStatusEvent)
    device_status_changed = Signal(DeviceStatusEvent)
    controller_status = Signal(str)
    command_response_received = Signal(dict)
    command_sending_result = Signal(dict)
    connection_status = Signal(dict)
    latency_changed = Signal(float)

    def __init__(self):
        """ ClimateControlModel constructor

        Initializes Flow library, thread pool
        """
        super(ClimateControlModel, self).__init__()
        self.__thread_pool = QThreadPool()
        self.__thread_pool.setMaxThreadCount(THREAD_POOL_MAX_THREADS)
        self.__flow_user = None
        self.__controller_device = None
        self.__checking_connection_status = False
        self.__last_measurement_event = None
        self.__last_relay_status_event = None
        self.__last_device_status_event = None
        self.__controller_heartbeat = None
        self.latency = Latency()
        self.__latency_timer = QTimer(self)
        self.__latency_timer.timeout.connect(self.__latency_timer_expired)
        ClimateControlModel.__message_received_signal = MessageReceived()
        ClimateControlModel.__message_received_signal.signal.connect(
            self.__message_received)

    def initialize(self, user_name, password):
        """ Initializes the system

        Initializes flow libraries and connects to server
        Logs in as user and tries to find the controller device, caches if found
        :param str user_name: flow user name
        :param str password: flow user password
        :raises ControllerNotFound, LoginFailed, FlowLibraryError
        """
        flow_url, flow_key, flow_secret = get_server_details()
        try:
            flow_initialize(flow_url, flow_key, flow_secret)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise FlowLibraryError("Failed to initialize flow library")

        try:
            self.__flow_user = FlowUser(user_name, password)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise LoginFailed()
        except ValueError as value_error:
            LOGGER.exception(value_error)
            raise LoginFailed()

        try:
            self.__controller_device = self.__flow_user.find_device(
                CONTROLLER_DEVICE_TYPE)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise ControllerNotFound()

        # enable message reception
        self.__flow_user.enable_message_reception(
            ClimateControlModel.message_received_callback)

    def close(self):
        """ De-initialize the model

        This function won't return till all the worker thread finish their job
        """
        LOGGER.debug("waiting for threads in thread pool")
        self.__thread_pool.waitForDone()
        LOGGER.debug("thread pool exit done")
        if self.__flow_user:
            self.__flow_user.logout()

    def get_settings(self):
        """ Get setting for the controller device

        Schedules a worker thread for getting setting
        """
        runnable = WorkerThread(get_setting_work,
                                self.__settings_received,
                                device=self.__controller_device,
                                key=ControllerSetting.controller_config_key)
        self.__thread_pool.start(runnable)

    def save_settings(self, setting):
        """ Save setting of controller device

        :param ControllerSetting setting: setting object to be saved in KVS
        """
        setting_xml = setting.to_xml()
        LOGGER.debug("Trying to save setting xml = {}".format(setting_xml))
        runnable = WorkerThread(save_setting_work,
                                self.__save_settings_result,
                                device=self.__controller_device,
                                key=ControllerSetting.controller_config_key,
                                value=setting_xml)
        self.__thread_pool.start(runnable)

    def send_command(self, command):
        """ Send command to the controller device

        :param ControllerCommandEnum command: command to be sent
        """
        if command not in ControllerCommandEnum:
            raise InvalidCommand("Command not supported")

        LOGGER.info("sending command {}".format(command.value))
        command_xml = ControllerCommand(command).xml
        LOGGER.debug("command xml {}".format(command_xml))
        runnable = WorkerThread(send_command_work,
                                self.__command_sent_result,
                                user=self.__flow_user,
                                device=self.__controller_device,
                                message=command_xml)
        self.__thread_pool.start(runnable)

    def check_connection_status(self):
        """ Starts worker thread for checking connection status
        """
        if not self.__checking_connection_status:
            self.__checking_connection_status = True
            runnable = WorkerThread(check_connection_status_work,
                                    self.__connection_status_result)
            self.__thread_pool.start(runnable)

    @Slot(str, str)
    def __settings_received(self, setting, error):
        """ Slot function called when get setting worker thread finishes the task

        :param str setting: received setting from flow
        :param str error: error string if any
        """
        parsed_setting = None
        if not error:
            LOGGER.debug("Received setting xml = {}".format(setting))
            try:
                parsed_setting = ControllerSetting(setting_xml=setting)
                if not self.__controller_heartbeat:
                    # start heartbeat timer with twice the heartbeat period so that even if some
                    # heartbeat message failed to receive, it should wait for another
                    # heartbeat message
                    self.__controller_heartbeat = \
                        HeartBeatTimer(parsed_setting.controller_heartbeat*2*1000,
                                       self.__heartbeat_timer_status_changed)
                else:
                    # if heartbeat already exist then restart with new time
                    self.__controller_heartbeat.change_timeout_period(
                        parsed_setting.controller_heartbeat * 2 * 1000)
            except ValueError as value_error:
                LOGGER.exception(value_error)
                error = "Invalid setting XML received"
        else:
            if error["connection_error"]:
                self.check_connection_status()

        self.setting_received.emit({"setting": parsed_setting, "error": error})

    @Slot(str)
    def __save_settings_result(self, error=None):
        """ Slot function for the save setting worker function result

        :param str error: error string if any
        """
        self.save_setting_result.emit({"error": error})
        if error:
            LOGGER.debug("Save setting failed: {}".format(error))
            if error["connection_error"]:
                self.check_connection_status()
        else:
            # if KVS updated then send message to device to update the settings
            self.send_command(ControllerCommandEnum.retrieve_settings)
            LOGGER.debug("Setting saved to KVS")

    @Slot(bool, bool)
    def __connection_status_result(self, network, internet):
        """ Slot function for connection status result
        :param bool network: False if network is down
        :param bool internet: False if internet is down
        """
        self.__checking_connection_status = False
        connection_status = {"network": network, "internet": internet}
        self.connection_status.emit(connection_status)

    @Slot(str)
    def __command_sent_result(self, error=None):
        """ Slot function called at the end of sending command

        :param str error: error string if any
        """
        if error:
            self.command_sending_result.emit({"error": error})
            LOGGER.debug("Message sending failed: {}".format(error))
            if error["connection_error"]:
                self.check_connection_status()
        else:
            LOGGER.debug("Message sending success")

# pylint: disable=invalid-name

    @Slot(str)
    def __heartbeat_timer_status_changed(self, status):
        """ Slot function called when heartbeat timer changes its status(start, expire etc)

        It receives controller status. on timer expire-controller status = "OFFLINE"
        on timer refresh(start)- controller status = "ONLINE"
        :param str status: "ONLINE" or "OFFLINE" status
        """
        self.controller_status.emit(status)
        if status == "OFFLINE":
            # timer expired so there might be something wrong with the
            # network or internet so check connection status
            self.check_connection_status()
            LOGGER.debug("Latency timer stopped")
            self.__latency_timer.stop()
            if self.__last_device_status_event:
                # if controller is OFFLINE, make sensor and actuator also OFFLINE
                self.__last_device_status_event.sensor_alive = False
                self.__last_device_status_event.actuator_alive = False
                self.device_status_changed.emit(
                    self.__last_device_status_event)
        else:
            self.__latency_timer.start(LATENCY_PERIOD * 1000)
            LOGGER.debug("Latency timer started")
# pylint: enable=invalid-name

    @Slot()
    def __latency_timer_expired(self):
        """ Slot called on the expiry of latency timer

        """
        # send ping message to controller and calculate latency based on when it is received
        self.send_command(ControllerCommandEnum.ping)

    def __handle_command_response(self, response_dict):
        """ Parse the received response and emit signal if valid response found

        :param dict response_dict: response xml dictionary
        """
        try:
            response = ControllerResponse(response_dict)
            LOGGER.info("received response {}".format(response.response.value))
            if response.response == ControllerResponseEnum.ping:
                # round_trip_time is the difference between current local time and timestamp
                # when message was sent, this includes any processing delay on controller side
                round_trip_time = (datetime.datetime.utcnow() -
                                   response.sent_time).total_seconds()
                LOGGER.debug("round_trip_time: {}".format(round_trip_time))
                # Ignore value where round_trip_time > MAX_LATENCY_TIME
                if round_trip_time <= MAX_LATENCY_VALUE:
                    latency = self.latency.calculate_exponential_moving_average(
                        round_trip_time)
                else:
                    latency = round_trip_time
                self.latency_changed.emit(latency)
                LOGGER.debug("Latency: {}".format(latency))
            else:
                self.command_response_received.emit(
                    {"response": response.response})

            # if retrieve_settings_success is received, get the settings again as it might
            # be update by some other app
            # for e.g. admin app updating threshold and display app updating its values accordingly
            if response.response == ControllerResponseEnum.retrieve_settings_success:
                self.get_settings()
        except ValueError as error:
            LOGGER.exception(error)

    def __process_heartbeat_event(self, event):
        """ Processes HeartBeat event data

        :param HeartBeatEvent event: HeartBeat event object
        """
        if self.__last_measurement_event:
            if not event.measurement_data == self.__last_measurement_event:
                self.measurement_changed.emit(event.measurement_data)
                self.__last_measurement_event = event.measurement_data
            else:
                LOGGER.debug("Ignoring measurement data")
        else:
            self.measurement_changed.emit(event.measurement_data)
            self.__last_measurement_event = event.measurement_data

        LOGGER.debug("Heartbeat Temp: {} Humidity: {} ".format(
            event.measurement_data.temperature,
            event.measurement_data.humidity))

        if self.__last_relay_status_event:
            if not event.relay_status == self.__last_relay_status_event:
                self.relay_status_changed.emit(event.relay_status)
                self.__last_relay_status_event = event.relay_status
            else:
                LOGGER.debug("Ignoring relay status data")
        else:
            self.relay_status_changed.emit(event.relay_status)
            self.__last_relay_status_event = event.relay_status

        if self.__last_device_status_event:
            if not event.device_status == self.__last_device_status_event:
                self.device_status_changed.emit(event.device_status)
                self.__last_device_status_event = event.device_status
            else:
                LOGGER.debug("Ignoring device status data")
        else:
            self.device_status_changed.emit(event.device_status)
            self.__last_device_status_event = event.device_status

        LOGGER.debug("Heartbeat Temp: {} Humidity: {} "
                     "Relay 1 ON: {} Relay 2 ON: {} "
                     "Sensor: {} Actuator: {}".format(
                         event.measurement_data.temperature,
                         event.measurement_data.humidity,
                         event.relay_status.relay_1_on,
                         event.relay_status.relay_2_on,
                         event.device_status.sensor_alive,
                         event.device_status.actuator_alive))

    def __handle_event(self, message_dict):
        """ Create event object according to event type and handle the event

        :param dict message_dict: message content in the dictionary format
        """
        try:
            event = ControllerEventFactory.create_event(message_dict)
            if self.__controller_heartbeat:
                self.__controller_heartbeat.refresh_timer()

            if isinstance(event, MeasurementEvent):
                self.measurement_changed.emit(event)
                self.__last_measurement_event = event
                LOGGER.debug("Measurement Temp: {} Humidity: {}".format(
                    event.temperature, event.humidity))
            elif isinstance(event, RelayStatusEvent):
                self.relay_status_changed.emit(event)
                self.__last_relay_status_event = event
                LOGGER.debug(
                    "RelayStatus Relay 1 ON: {} Relay 2 ON: {}".format(
                        event.relay_1_on, event.relay_2_on))
            elif isinstance(event, DeviceStatusEvent):
                self.device_status_changed.emit(event)
                self.__last_device_status_event = event
                LOGGER.debug("DeviceStatus Sensor: {} Actuator: {}".format(
                    event.sensor_alive, event.actuator_alive))
            elif isinstance(event, HeartBeatEvent):
                self.__process_heartbeat_event(event)

        except ValueError as value_error:
            LOGGER.exception(value_error)

    @Slot(str)
    def __message_received(self, message_content):
        """ Slot function called when message is received by callback function

        This function parses message_content(xml_string) into dictionary
        :param str message_content: message content which is xml string
        """
        LOGGER.debug("Received message {}".format(message_content))
        # We have received a message from controller means network and internet are up
        status = {"network": True, "internet": True}
        self.connection_status.emit(status)
        try:
            message_dict = xmltodict.parse(message_content)
            root_tag = message_dict.keys()[0]
            if root_tag == "response":
                self.__handle_command_response(message_dict)
            elif root_tag == "event":
                self.__handle_event(message_dict)
            else:
                LOGGER.error(
                    "Unsupported message received {}".format(message_content))
        except (ExpatError, KeyError) as error:
            LOGGER.exception("Error in parsing xml {} xml={}".format(
                error.message, message_content))

    @staticmethod
    def message_received_callback(flow_message):
        """ Callback called by library, libflow expects this to be a static class method

        :param flow_message: message object from library
        """
        message = FlowMessage(flow_message)
        message_content = message.get_message_content()
        ClimateControlModel.__message_received_signal.signal.emit(
            message_content)
class TestFlowUser(unittest.TestCase):
    """ Testing of FlowUser class members
    """
    def setUp(self):
        flow_initialize(flow_server_url, flow_server_key, flow_server_secret)
        self.user = None

    def test_login(self):
        """ Test passes when user login succeed
        """
        self.user = FlowUser(username, password)

    def test_login_none_parameter_raises_value_exception(self):
        """ Test passes when user login raises ValueError
        """
        with self.assertRaises(ValueError):
            self.user = FlowUser(username, "")

    def test_login_wrong_parameters_raises_flow_exception(self):
        """ Test passes when user login raises FlowCoreException
        """
        with self.assertRaises(FlowCoreException):
            self.user = FlowUser("unknown_user", "123")

    def test_logout(self):
        """ Test to check logout()
        """
        self.user = FlowUser(username, password)
        self.user.logout()

    def test_enable_message_reception(self):
        """ Test to check enable_message_reception()
        """
        self.user = FlowUser(username, password)
        self.user.enable_message_reception("callback")

    def test_enable_message_reception_none_parameter__raises_value_error(self):
        """ Test passes when enable_message_reception() raises ValueError
        """
        self.user = FlowUser(username, password)
        with self.assertRaises(ValueError):
            self.user.enable_message_reception("")

    def test_find_device_expected(self):
        """ Test to check find_device()
        """
        self.user = FlowUser(username, password)
        self.assertIsNotNone(self.user.find_device(device_type))

    def test_find_device_none_parameter_raises_value_error(self):
        """ Test passes when find_device() raises ValueError
        """
        self.user = FlowUser(username, password)
        with self.assertRaises(ValueError):
            self.user.find_device("")

    def test_find_device_wrong_parameter_raises_flow_exception(self):
        """Test passes when no device of particular type found
        """
        self.user = FlowUser(username, password)
        with self.assertRaises(FlowException):
            self.user.find_device("unknown_device")

    def test_get_owned_devices_expected(self):
        """ Test passes when at least one device found
        """
        self.user = FlowUser(username, password)
        self.assertIsNotNone(self.user.get_owned_devices())

    def test_send_message(self):
        """ Test for sending message to device
        """
        self.user = FlowUser(username, password)
        device = self.user.find_device(device_type)
        self.user.send_message_to_device(device, "Hi", 20)

    def test_send_message_none_parameter_raises_value_error(self):
        """ Test passes when send_message_to_device() raises ValueError
        """
        self.user = FlowUser(username, password)
        device = self.user.find_device(device_type)
        with self.assertRaises(ValueError):
            self.user.send_message_to_device(device, "", 20)
 def test_login_wrong_parameters_raises_flow_exception(self):
     """ Test passes when user login raises FlowCoreException
     """
     with self.assertRaises(FlowCoreException):
         self.user = FlowUser("unknown_user", "123")
 def test_login_none_parameter_raises_value_exception(self):
     """ Test passes when user login raises ValueError
     """
     with self.assertRaises(ValueError):
         self.user = FlowUser(username, "")
 def test_logout(self):
     """ Test to check logout()
     """
     self.user = FlowUser(username, password)
     self.user.logout()
 def test_enable_message_reception(self):
     """ Test to check enable_message_reception()
     """
     self.user = FlowUser(username, password)
     self.user.enable_message_reception("callback")
class ClimateControlModel(QObject):
    """ Model class representing the climate control system

    Talks with flow, fetches required data from flow, caches some of the data
    """
    __message_received_signal = None
    setting_received = Signal(dict)
    save_setting_result = Signal(dict)
    measurement_changed = Signal(MeasurementEvent)
    relay_status_changed = Signal(RelayStatusEvent)
    device_status_changed = Signal(DeviceStatusEvent)
    controller_status = Signal(str)
    command_response_received = Signal(dict)
    command_sending_result = Signal(dict)
    connection_status = Signal(dict)
    latency_changed = Signal(float)

    def __init__(self):
        """ ClimateControlModel constructor

        Initializes Flow library, thread pool
        """
        super(ClimateControlModel, self).__init__()
        self.__thread_pool = QThreadPool()
        self.__thread_pool.setMaxThreadCount(THREAD_POOL_MAX_THREADS)
        self.__flow_user = None
        self.__controller_device = None
        self.__checking_connection_status = False
        self.__last_measurement_event = None
        self.__last_relay_status_event = None
        self.__last_device_status_event = None
        self.__controller_heartbeat = None
        self.latency = Latency()
        self.__latency_timer = QTimer(self)
        self.__latency_timer.timeout.connect(self.__latency_timer_expired)
        ClimateControlModel.__message_received_signal = MessageReceived()
        ClimateControlModel.__message_received_signal.signal.connect(self.__message_received)

    def initialize(self, user_name, password):
        """ Initializes the system

        Initializes flow libraries and connects to server
        Logs in as user and tries to find the controller device, caches if found
        :param str user_name: flow user name
        :param str password: flow user password
        :raises ControllerNotFound, LoginFailed, FlowLibraryError
        """
        flow_url, flow_key, flow_secret = get_server_details()
        try:
            flow_initialize(flow_url, flow_key, flow_secret)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise FlowLibraryError("Failed to initialize flow library")

        try:
            self.__flow_user = FlowUser(user_name, password)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise LoginFailed()
        except ValueError as value_error:
            LOGGER.exception(value_error)
            raise LoginFailed()

        try:
            self.__controller_device = self.__flow_user.find_device(CONTROLLER_DEVICE_TYPE)
        except FlowException as flow_error:
            if flow_error.connection_error:
                self.check_connection_status()
            LOGGER.exception(flow_error)
            raise ControllerNotFound()

        # enable message reception
        self.__flow_user.enable_message_reception(ClimateControlModel.message_received_callback)

    def close(self):
        """ De-initialize the model

        This function won't return till all the worker thread finish their job
        """
        LOGGER.debug("waiting for threads in thread pool")
        self.__thread_pool.waitForDone()
        LOGGER.debug("thread pool exit done")
        if self.__flow_user:
            self.__flow_user.logout()

    def get_settings(self):
        """ Get setting for the controller device

        Schedules a worker thread for getting setting
        """
        runnable = WorkerThread(get_setting_work, self.__settings_received,
                                device=self.__controller_device,
                                key=ControllerSetting.controller_config_key)
        self.__thread_pool.start(runnable)

    def save_settings(self, setting):
        """ Save setting of controller device

        :param ControllerSetting setting: setting object to be saved in KVS
        """
        setting_xml = setting.to_xml()
        LOGGER.debug("Trying to save setting xml = {}".format(setting_xml))
        runnable = WorkerThread(save_setting_work, self.__save_settings_result,
                                device=self.__controller_device,
                                key=ControllerSetting.controller_config_key,
                                value=setting_xml)
        self.__thread_pool.start(runnable)

    def send_command(self, command):
        """ Send command to the controller device

        :param ControllerCommandEnum command: command to be sent
        """
        if command not in ControllerCommandEnum:
            raise InvalidCommand("Command not supported")

        LOGGER.info("sending command {}".format(command.value))
        command_xml = ControllerCommand(command).xml
        LOGGER.debug("command xml {}".format(command_xml))
        runnable = WorkerThread(send_command_work, self.__command_sent_result,
                                user=self.__flow_user, device=self.__controller_device,
                                message=command_xml)
        self.__thread_pool.start(runnable)

    def check_connection_status(self):
        """ Starts worker thread for checking connection status
        """
        if not self.__checking_connection_status:
            self.__checking_connection_status = True
            runnable = WorkerThread(check_connection_status_work, self.__connection_status_result)
            self.__thread_pool.start(runnable)

    @Slot(str, str)
    def __settings_received(self, setting, error):
        """ Slot function called when get setting worker thread finishes the task

        :param str setting: received setting from flow
        :param str error: error string if any
        """
        parsed_setting = None
        if not error:
            LOGGER.debug("Received setting xml = {}".format(setting))
            try:
                parsed_setting = ControllerSetting(setting_xml=setting)
                if not self.__controller_heartbeat:
                    # start heartbeat timer with twice the heartbeat period so that even if some
                    # heartbeat message failed to receive, it should wait for another
                    # heartbeat message
                    self.__controller_heartbeat = \
                        HeartBeatTimer(parsed_setting.controller_heartbeat*2*1000,
                                       self.__heartbeat_timer_status_changed)
                else:
                    # if heartbeat already exist then restart with new time
                    self.__controller_heartbeat.change_timeout_period(
                        parsed_setting.controller_heartbeat*2*1000)
            except ValueError as value_error:
                LOGGER.exception(value_error)
                error = "Invalid setting XML received"
        else:
            if error["connection_error"]:
                self.check_connection_status()

        self.setting_received.emit({"setting": parsed_setting, "error": error})

    @Slot(str)
    def __save_settings_result(self, error=None):
        """ Slot function for the save setting worker function result

        :param str error: error string if any
        """
        self.save_setting_result.emit({"error": error})
        if error:
            LOGGER.debug("Save setting failed: {}".format(error))
            if error["connection_error"]:
                self.check_connection_status()
        else:
            # if KVS updated then send message to device to update the settings
            self.send_command(ControllerCommandEnum.retrieve_settings)
            LOGGER.debug("Setting saved to KVS")

    @Slot(bool, bool)
    def __connection_status_result(self, network, internet):
        """ Slot function for connection status result
        :param bool network: False if network is down
        :param bool internet: False if internet is down
        """
        self.__checking_connection_status = False
        connection_status = {"network": network, "internet": internet}
        self.connection_status.emit(connection_status)

    @Slot(str)
    def __command_sent_result(self, error=None):
        """ Slot function called at the end of sending command

        :param str error: error string if any
        """
        if error:
            self.command_sending_result.emit({"error": error})
            LOGGER.debug("Message sending failed: {}".format(error))
            if error["connection_error"]:
                self.check_connection_status()
        else:
            LOGGER.debug("Message sending success")

# pylint: disable=invalid-name
    @Slot(str)
    def __heartbeat_timer_status_changed(self, status):
        """ Slot function called when heartbeat timer changes its status(start, expire etc)

        It receives controller status. on timer expire-controller status = "OFFLINE"
        on timer refresh(start)- controller status = "ONLINE"
        :param str status: "ONLINE" or "OFFLINE" status
        """
        self.controller_status.emit(status)
        if status == "OFFLINE":
            # timer expired so there might be something wrong with the
            # network or internet so check connection status
            self.check_connection_status()
            LOGGER.debug("Latency timer stopped")
            self.__latency_timer.stop()
            if self.__last_device_status_event:
                # if controller is OFFLINE, make sensor and actuator also OFFLINE
                self.__last_device_status_event.sensor_alive = False
                self.__last_device_status_event.actuator_alive = False
                self.device_status_changed.emit(self.__last_device_status_event)
        else:
            self.__latency_timer.start(LATENCY_PERIOD*1000)
            LOGGER.debug("Latency timer started")
# pylint: enable=invalid-name

    @Slot()
    def __latency_timer_expired(self):
        """ Slot called on the expiry of latency timer

        """
        # send ping message to controller and calculate latency based on when it is received
        self.send_command(ControllerCommandEnum.ping)

    def __handle_command_response(self, response_dict):
        """ Parse the received response and emit signal if valid response found

        :param dict response_dict: response xml dictionary
        """
        try:
            response = ControllerResponse(response_dict)
            LOGGER.info("received response {}".format(response.response.value))
            if response.response == ControllerResponseEnum.ping:
                # round_trip_time is the difference between current local time and timestamp
                # when message was sent, this includes any processing delay on controller side
                round_trip_time = (datetime.datetime.utcnow() - response.sent_time).total_seconds()
                LOGGER.debug("round_trip_time: {}".format(round_trip_time))
                # Ignore value where round_trip_time > MAX_LATENCY_TIME
                if round_trip_time <= MAX_LATENCY_VALUE:
                    latency = self.latency.calculate_exponential_moving_average(round_trip_time)
                else:
                    latency = round_trip_time
                self.latency_changed.emit(latency)
                LOGGER.debug("Latency: {}".format(latency))
            else:
                self.command_response_received.emit({"response": response.response})

            # if retrieve_settings_success is received, get the settings again as it might
            # be update by some other app
            # for e.g. admin app updating threshold and display app updating its values accordingly
            if response.response == ControllerResponseEnum.retrieve_settings_success:
                self.get_settings()
        except ValueError as error:
            LOGGER.exception(error)

    def __process_heartbeat_event(self, event):
        """ Processes HeartBeat event data

        :param HeartBeatEvent event: HeartBeat event object
        """
        if self.__last_measurement_event:
            if not event.measurement_data == self.__last_measurement_event:
                self.measurement_changed.emit(event.measurement_data)
                self.__last_measurement_event = event.measurement_data
            else:
                LOGGER.debug("Ignoring measurement data")
        else:
            self.measurement_changed.emit(event.measurement_data)
            self.__last_measurement_event = event.measurement_data

        LOGGER.debug("Heartbeat Temp: {} Humidity: {} ".format(event.measurement_data.temperature,
                                                               event.measurement_data.humidity))

        if self.__last_relay_status_event:
            if not event.relay_status == self.__last_relay_status_event:
                self.relay_status_changed.emit(event.relay_status)
                self.__last_relay_status_event = event.relay_status
            else:
                LOGGER.debug("Ignoring relay status data")
        else:
            self.relay_status_changed.emit(event.relay_status)
            self.__last_relay_status_event = event.relay_status

        if self.__last_device_status_event:
            if not event.device_status == self.__last_device_status_event:
                self.device_status_changed.emit(event.device_status)
                self.__last_device_status_event = event.device_status
            else:
                LOGGER.debug("Ignoring device status data")
        else:
            self.device_status_changed.emit(event.device_status)
            self.__last_device_status_event = event.device_status

        LOGGER.debug("Heartbeat Temp: {} Humidity: {} "
                     "Relay 1 ON: {} Relay 2 ON: {} "
                     "Sensor: {} Actuator: {}".format(event.measurement_data.temperature,
                                                      event.measurement_data.humidity,
                                                      event.relay_status.relay_1_on,
                                                      event.relay_status.relay_2_on,
                                                      event.device_status.sensor_alive,
                                                      event.device_status.actuator_alive))

    def __handle_event(self, message_dict):
        """ Create event object according to event type and handle the event

        :param dict message_dict: message content in the dictionary format
        """
        try:
            event = ControllerEventFactory.create_event(message_dict)
            if self.__controller_heartbeat:
                self.__controller_heartbeat.refresh_timer()

            if isinstance(event, MeasurementEvent):
                self.measurement_changed.emit(event)
                self.__last_measurement_event = event
                LOGGER.debug("Measurement Temp: {} Humidity: {}".format(event.temperature,
                                                                        event.humidity))
            elif isinstance(event, RelayStatusEvent):
                self.relay_status_changed.emit(event)
                self.__last_relay_status_event = event
                LOGGER.debug("RelayStatus Relay 1 ON: {} Relay 2 ON: {}".format(event.relay_1_on,
                                                                                event.relay_2_on))
            elif isinstance(event, DeviceStatusEvent):
                self.device_status_changed.emit(event)
                self.__last_device_status_event = event
                LOGGER.debug("DeviceStatus Sensor: {} Actuator: {}".format(event.sensor_alive,
                                                                           event.actuator_alive))
            elif isinstance(event, HeartBeatEvent):
                self.__process_heartbeat_event(event)

        except ValueError as value_error:
            LOGGER.exception(value_error)

    @Slot(str)
    def __message_received(self, message_content):
        """ Slot function called when message is received by callback function

        This function parses message_content(xml_string) into dictionary
        :param str message_content: message content which is xml string
        """
        LOGGER.debug("Received message {}".format(message_content))
        # We have received a message from controller means network and internet are up
        status = {"network": True, "internet": True}
        self.connection_status.emit(status)
        try:
            message_dict = xmltodict.parse(message_content)
            root_tag = message_dict.keys()[0]
            if root_tag == "response":
                self.__handle_command_response(message_dict)
            elif root_tag == "event":
                self.__handle_event(message_dict)
            else:
                LOGGER.error("Unsupported message received {}".format(message_content))
        except (ExpatError, KeyError) as error:
            LOGGER.exception("Error in parsing xml {} xml={}".
                             format(error.message, message_content))

    @staticmethod
    def message_received_callback(flow_message):
        """ Callback called by library, libflow expects this to be a static class method

        :param flow_message: message object from library
        """
        message = FlowMessage(flow_message)
        message_content = message.get_message_content()
        ClimateControlModel.__message_received_signal.signal.emit(message_content)
 def test_find_device_none_parameter_raises_value_error(self):
     """ Test passes when find_device() raises ValueError
     """
     self.user = FlowUser(username, password)
     with self.assertRaises(ValueError):
         self.user.find_device("")
 def test_find_device_expected(self):
     """ Test to check find_device()
     """
     self.user = FlowUser(username, password)
     self.assertIsNotNone(self.user.find_device(device_type))
 def test_enable_message_reception_none_parameter__raises_value_error(self):
     """ Test passes when enable_message_reception() raises ValueError
     """
     self.user = FlowUser(username, password)
     with self.assertRaises(ValueError):
         self.user.enable_message_reception("")
 def test_get_owned_devices_expected(self):
     """ Test passes when at least one device found
     """
     self.user = FlowUser(username, password)
     self.assertIsNotNone(self.user.get_owned_devices())
 def test_send_message(self):
     """ Test for sending message to device
     """
     self.user = FlowUser(username, password)
     device = self.user.find_device(device_type)
     self.user.send_message_to_device(device, "Hi", 20)
 def setUp(self):
     flow_initialize(flow_server_url, flow_server_key, flow_server_secret)
     self.user = FlowUser(username, password)
     self.device = self.user.find_device(device_type)
 def test_login(self):
     """ Test passes when user login succeed
     """
     self.user = FlowUser(username, password)