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 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)