def device_state_report_handler(message, listener): """Handle a device trying to report it's state Todo: AWS iot has other error codes for this, copied from HTTP error codes. we could do that as well """ try: message.device.update_reported_state_from_message(message) except exceptions.BadMessageSchemaError: logger.exception("Device tried to update state with incorrect schema") wrapped = { "body": { "status": "failure", "error": "Invalid message schema", }, "device": message.device, "category": "reported_state_failure", "timestamp": datetime.datetime.utcnow(), } response = Message.from_dict(wrapped) response.send_to_device() raise except Exception: logger.exception("Unknown error processing") wrapped = { "body": { "status": "failure", "error": "Unknown error", }, "device": message.device, "category": "reported_state_failure", "timestamp": datetime.datetime.utcnow(), } response = Message.from_dict(wrapped) response.send_to_device() raise else: logger.info("successfully processed device reporting state update") wrapped = { "body": { "status": "success", }, "device": message.device, "category": "reported_state_success", "timestamp": datetime.datetime.utcnow(), } response = Message.from_dict(wrapped) response.send_to_device()
def test_to_device(self, fakedevice): with mock.patch(IBM + ".connect"), \ mock.patch(IBM + ".send_message") as send: sender = get_sender() sender.to_device("cat", {"a": "b"}, device_id=fakedevice.get_iot_id(), device_type=fakedevice.product.iot_name) send.assert_called_once_with( "cat", {"a": "b"}, device_id=str(fakedevice.id), device_type=fakedevice.product.iot_name) send.reset_mock() sender.to_device("cat", {"a": "b"}, device=fakedevice) send.assert_called_once_with("cat", {"a": "b"}, device_id=str(fakedevice.id), device_type="testproduct123") send.reset_mock() sender.to_device("example-category", {"foo": "bar"}) send.assert_not_called() send.reset_mock() message = Message("incoming-category", {"a": "b"}, fakedevice) sender.to_device("cat", {"a": "b"}, incoming_message=message) send.assert_called_once_with("cat", {"a": "b"}, device_id=str(fakedevice.id), device_type="testproduct123")
def send_ts_data(self, serializer, device): """ Create a ZConnect Message from the incoming data and process with the celery worker. """ timestamp = serializer.data["timestamp"] if not isinstance(timestamp, datetime): timestamp = parser.parse(timestamp) message = Message( category="periodic", device=device, body=serializer.data["data"], timestamp=timestamp, ) process_message.apply_async(args=[message.as_dict()])
def test_message_callback(self, fakedevice): with mock.patch(IBM + ".connect"): greenlet = get_listener() mocked = mock.Mock() message = Message("cat", {"a": "b"}, fakedevice) greenlet.client.message_handlers = {"cat": [mocked]} greenlet.client._message_callback(message) mocked.assert_called_once_with(message, greenlet.client)
def get_message(device, event_def): return Message( category="event", device=device, body={ "event_id": "{}:{}".format(device.id, event_def.id), "source": "server", "current": device.get_latest_ts_data() }, )
def test_generate_event_callback(self, fakedevice, interface): with mock.patch(IBM + ".construct_zconnect_message") as construct: mocked = mock.Mock() message = Message("cat", {"a": "b"}, fakedevice) construct.return_value = message callback = interface.generate_event_callback(mocked) callback("test") mocked.assert_called_once_with(message)
def fix_fake_message(self, fakedevice): test_message = Message.from_dict({ "category": "report_state", "timestamp": datetime.datetime.utcnow().isoformat(), "device": fakedevice.id, "body": { "tag": 123, } }) return test_message
def update_reported_state(self, new_state, verify=False): """Given a new reported state from a device, create a new DeviceState object in the database If the reported state changes between us reading the last state and us trying to save a new state, there is another race condition somewhere which is a fairly fatal error which we can't do anything about. We don't care if the desired state changes though, as that should be handled somewhere else or on the device Args: new_state (dict): The 'reported' state from the device. This should be the RAW state, ie no 'state' or 'reported' key verify (bool): If True, verification will be done to make sure it matches the expected Product state layout. If not, assume it has already been validated. Raises: BadMessageSchemaError: If verify is True and message validation fails Returns: DeviceState: new state document """ if verify: wrapped = { "body": { "state": { "reported": new_state }, }, "device": self.pk, "category": "reported_state", "timestamp": datetime.datetime.utcnow(), } message = Message.from_dict(wrapped) verify_message_schema(message) return self._attempt_device_state_update("reported", new_state)
def update_desired_state(self, new_state, verify=True, error_on_reported_change=False): """Given a new desired state for a device, create a new DeviceState object in the database If the state update succeeds, it will send a message to the device to tell it that it should try and change to the desired state If the state is updated but it raises an integrity error, another thread/process might have changed the state. In this case, if it is just the device reporting that it's state has changed and error_on_reported_change is True, raise an error. This will always be raised if the desired state changes. If the reported state changes but error_on_reported_change is False, just set the new state document to contain that reported state and return to the user. note that this does hide information from the developer, but the reported state change should be handled in another process anyway. Todo: What if it can save to the DB, but the MQTT message can't be sent? If we send the message before saving the state there is a race condition, but if we save the state before sending we need to buffer the 'change state' message somehow so it can be sent Args: new_state (dict): The new 'desired' state for the device. This should be the RAW state, ie no 'state' or 'reported' key verify (bool): If True, verification will be done to make sure it matches the expected Product state layout. If not, assume it has already been validated. Raises: BadMessageSchemaError: If verify is True and message validation fails StateConflictError: If the state is changed elsewhere while trying to save a new state (according to rules described above) Returns: DeviceState: new state document """ wrapped = { "body": { "state": { "desired": new_state }, }, "device": self.pk, "category": "desired_state", "timestamp": datetime.datetime.utcnow(), } if verify: v_dict = copy.deepcopy(wrapped) v_dict["body"]["state"]["reported"] = v_dict["body"]["state"].pop( "desired") v_msg = Message.from_dict(v_dict) verify_message_schema(v_msg) new_latest_state = self._attempt_device_state_update( "desired", new_state, error_on_reported_change) message = Message.from_dict(wrapped) message.send_to_device() return new_latest_state