From d25578ebdbb637947a7d0efef7a0fcad7c490d1e Mon Sep 17 00:00:00 2001 From: Roland Meyer Date: Wed, 5 Oct 2022 20:48:25 +0200 Subject: [PATCH] Added option to send MQTT messages --- HTTPShortcuts/app/build.gradle.kts | 2 + .../app/src/main/assets/docs/scripting.html | 6 +- .../GenerateCodeSnippetItemsUseCase.kt | 11 ++++ .../scripting/actions/ActionDTO.kt | 3 + .../scripting/actions/ActionFactory.kt | 2 + .../actions/types/SendMQTTMessagesAction.kt | 51 +++++++++++++++++ .../types/SendMQTTMessagesActionType.kt | 55 +++++++++++++++++++ .../actions/types/SendTCPPacketAction.kt | 1 - .../app/src/main/res/values/strings.xml | 2 + 9 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesAction.kt create mode 100644 HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesActionType.kt diff --git a/HTTPShortcuts/app/build.gradle.kts b/HTTPShortcuts/app/build.gradle.kts index 56958d668..8f109a86d 100644 --- a/HTTPShortcuts/app/build.gradle.kts +++ b/HTTPShortcuts/app/build.gradle.kts @@ -218,6 +218,8 @@ dependencies { implementation("com.github.franmontiel:PersistentCookieJar:v1.0.1") implementation("org.conscrypt:conscrypt-android:2.5.2") + implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0") + /* Scheduling */ implementation("androidx.work:work-runtime-ktx:2.7.1") implementation("androidx.work:work-rxjava2:2.7.1") diff --git a/HTTPShortcuts/app/src/main/assets/docs/scripting.html b/HTTPShortcuts/app/src/main/assets/docs/scripting.html index 7e7f24452..ed21eaaed 100644 --- a/HTTPShortcuts/app/src/main/assets/docs/scripting.html +++ b/HTTPShortcuts/app/src/main/assets/docs/scripting.html @@ -102,7 +102,11 @@

Wake-on-LAN

You can use the wakeOnLan function to send a magic packet to turn on another device on your network. The first parameter has to be the MAC-address of the device. As the optional second parameter, you can pass the network/broadcast address to be used, and as the third parameter you can define the port.

wakeOnLan('01-23-45-67-89-ab');
 
 wakeOnLan('01-23-45-67-89-ab', '255.255.255.255', 9);
-

Send TCP Packet

You can use the sendTCPPacket function to send a TCP packet to another device on your network. Pass the packet data as the first parameter (either as a string, Uint8Array or array of numbers denoting bytes), the target host's name or IP address as the second parameter and its TCP port as the third parameter.

sendTCPPacket('hello', '192.168.1.42', 1337);
+

Send MQTT message

The sendMqttMessages function allows you to connect to an MQTT broker, send (i.e. publish) one or more messages to it, and then disconnect again. The first parameter is the URI of the server/broker, the second (optional) parameter provides options for the connection (e.g. username and password) and the third parameter is a list of all the messages that should be sent.

sendMQTTMessages("tcp://192.168.0.42:1234", {"username": "admin", "password": "1234"}, [
+    {"topic": "hallway-lamp/set", "payload": "{\"state\":\"ON\"}"},
+    {"topic": "desk-lamp/set", "payload": "{\"state\":\"ON\", \"brightness\": 255}"},
+]);
+

Please note that this does not provide any particular quality of service guarantees, and that it is not possible to subscribe to topics this way.

Send TCP Packet

You can use the sendTCPPacket function to send a TCP packet to another device on your network. Pass the packet data as the first parameter (either as a string, Uint8Array or array of numbers denoting bytes), the target host's name or IP address as the second parameter and its TCP port as the third parameter.

sendTCPPacket('hello', '192.168.1.42', 1337);
 
 sendTCPPacket([0x68, 0x65, 0x6C, 0x6C, 0x6F], 'example.com', 4242);
 

Send UDP Packet

You can use the sendUDPPacket function to send a UDP packet to another device on your network. Pass the packet data as the first parameter (either as a string, Uint8Array or array of numbers denoting bytes), the target host's name or IP address as the second parameter and its UDP port as the third parameter.

sendUDPPacket('hello', '192.168.1.42', 1337);
diff --git a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/activities/editor/scripting/codesnippets/usecases/GenerateCodeSnippetItemsUseCase.kt b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/activities/editor/scripting/codesnippets/usecases/GenerateCodeSnippetItemsUseCase.kt
index 09c4fdd6a..068b4debe 100644
--- a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/activities/editor/scripting/codesnippets/usecases/GenerateCodeSnippetItemsUseCase.kt
+++ b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/activities/editor/scripting/codesnippets/usecases/GenerateCodeSnippetItemsUseCase.kt
@@ -415,6 +415,17 @@ constructor(
                 ) {
                     insertText("wakeOnLan(\"", "\");\n")
                 }
+                item(
+                    R.string.action_type_send_mqtt_message,
+                    docRef = "send-mqtt-message",
+                    keywords = setOf("network", "client", "publish"),
+                ) {
+                    insertText(
+                        "sendMQTTMessages(\"tcp://broker:port\", {\"username\": \"\", \"password\": \"\"}, [\n    " +
+                            "{\"topic\": \"\", \"payload\": \"\"},\n]};\n",
+                        "",
+                    )
+                }
                 item(
                     R.string.action_type_send_tcp_packet,
                     docRef = "send-tcp-packet",
diff --git a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionDTO.kt b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionDTO.kt
index 5349d2dc1..f616a3b5e 100644
--- a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionDTO.kt
+++ b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionDTO.kt
@@ -11,6 +11,9 @@ class ActionDTO(
     val data: List = emptyList(),
 ) {
 
+    val argCount: Int
+        get() = data.size
+
     fun getString(index: Int): String? {
         val value = data.getOrNull(index)
         return when {
diff --git a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionFactory.kt b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionFactory.kt
index e82c37ed2..1cf66ba75 100644
--- a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionFactory.kt
+++ b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/ActionFactory.kt
@@ -25,6 +25,7 @@ import ch.rmy.android.http_shortcuts.scripting.actions.types.RenameShortcutActio
 import ch.rmy.android.http_shortcuts.scripting.actions.types.ScanBarcodeActionType
 import ch.rmy.android.http_shortcuts.scripting.actions.types.SelectionActionType
 import ch.rmy.android.http_shortcuts.scripting.actions.types.SendIntentActionType
+import ch.rmy.android.http_shortcuts.scripting.actions.types.SendMQTTMessagesActionType
 import ch.rmy.android.http_shortcuts.scripting.actions.types.SendTCPPacketActionType
 import ch.rmy.android.http_shortcuts.scripting.actions.types.SendUDPPacketActionType
 import ch.rmy.android.http_shortcuts.scripting.actions.types.SetVariableActionType
@@ -80,6 +81,7 @@ class ActionFactory {
                 ScanBarcodeActionType(),
                 SelectionActionType(),
                 SendIntentActionType(),
+                SendMQTTMessagesActionType(),
                 SendTCPPacketActionType(),
                 SendUDPPacketActionType(),
                 SetVariableActionType(),
diff --git a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesAction.kt b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesAction.kt
new file mode 100644
index 000000000..fb704912d
--- /dev/null
+++ b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesAction.kt
@@ -0,0 +1,51 @@
+package ch.rmy.android.http_shortcuts.scripting.actions.types
+
+import ch.rmy.android.framework.extensions.applyIfNotNull
+import ch.rmy.android.framework.extensions.logException
+import ch.rmy.android.http_shortcuts.exceptions.ActionException
+import ch.rmy.android.http_shortcuts.scripting.ExecutionContext
+import io.reactivex.Completable
+import io.reactivex.schedulers.Schedulers
+import org.eclipse.paho.client.mqttv3.MqttClient
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions
+import org.eclipse.paho.client.mqttv3.MqttException
+import org.eclipse.paho.client.mqttv3.MqttMessage
+
+class SendMQTTMessagesAction(
+    private val serverUri: String,
+    private val username: String?,
+    private val password: String?,
+    private val messages: List,
+) : BaseAction() {
+
+    override fun execute(executionContext: ExecutionContext): Completable =
+        Completable.fromAction {
+            try {
+                val client = MqttClient(serverUri, MqttClient.generateClientId(), null)
+                val options = MqttConnectOptions()
+                    .apply {
+                        isCleanSession = true
+                    }
+                    .applyIfNotNull(username) {
+                        userName = it
+                    }
+                    .applyIfNotNull(password) {
+                        password = it.toCharArray()
+                    }
+                client.connect(options)
+                messages.forEach { message ->
+                    client.publish(message.topic, MqttMessage(message.payload))
+                }
+                client.disconnect()
+                client.close()
+            } catch (e: MqttException) {
+                logException(e)
+                throw ActionException {
+                    "Failed to send MQTT message: $e"
+                }
+            }
+        }
+            .subscribeOn(Schedulers.io())
+
+    data class Message(val topic: String, val payload: ByteArray)
+}
diff --git a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesActionType.kt b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesActionType.kt
new file mode 100644
index 000000000..e296da7f3
--- /dev/null
+++ b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendMQTTMessagesActionType.kt
@@ -0,0 +1,55 @@
+package ch.rmy.android.http_shortcuts.scripting.actions.types
+
+import ch.rmy.android.http_shortcuts.scripting.ActionAlias
+import ch.rmy.android.http_shortcuts.scripting.actions.ActionDTO
+import org.json.JSONObject
+import org.liquidplayer.javascript.JSValue
+
+class SendMQTTMessagesActionType : BaseActionType() {
+
+    override val type = TYPE
+
+    override fun fromDTO(actionDTO: ActionDTO): SendMQTTMessagesAction {
+        val options = if (actionDTO.argCount >= 3) {
+            actionDTO.getObject(1)
+        } else {
+            null
+        }
+        val messages = if (actionDTO.argCount >= 3) {
+            actionDTO.getList(2)
+        } else {
+            actionDTO.getList(1)
+        }
+            .orEmpty()
+            .mapNotNull {
+                when (it) {
+                    is JSValue -> {
+                        val obj = JSONObject(it.toJSON())
+                        SendMQTTMessagesAction.Message(
+                            topic = obj.getString("topic"),
+                            payload = obj.getString("payload").toByteArray(),
+                        )
+                    }
+                    else -> null
+                }
+            }
+
+        return SendMQTTMessagesAction(
+            serverUri = actionDTO.getString(0) ?: "",
+            username = options?.get("username") as? String,
+            password = options?.get("password") as? String,
+            messages = messages,
+        )
+    }
+
+    override fun getAlias() = ActionAlias(
+        functionName = FUNCTION_NAME,
+        functionNameAliases = setOf("sendMQTTMessage", "sendMqttMessages", "sendMqttMessage"),
+        parameters = 3,
+    )
+
+    companion object {
+        private const val TYPE = "send_mqtt_messages"
+        private const val FUNCTION_NAME = "sendMQTTMessages"
+    }
+}
diff --git a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendTCPPacketAction.kt b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendTCPPacketAction.kt
index 88727b4a3..d9df707a5 100644
--- a/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendTCPPacketAction.kt
+++ b/HTTPShortcuts/app/src/main/kotlin/ch/rmy/android/http_shortcuts/scripting/actions/types/SendTCPPacketAction.kt
@@ -8,7 +8,6 @@ import io.reactivex.schedulers.Schedulers
 import java.net.InetAddress
 import java.net.Socket
 
-
 class SendTCPPacketAction(
     private val data: ByteArray,
     private val ipAddress: String,
diff --git a/HTTPShortcuts/app/src/main/res/values/strings.xml b/HTTPShortcuts/app/src/main/res/values/strings.xml
index 909606e69..468545562 100644
--- a/HTTPShortcuts/app/src/main/res/values/strings.xml
+++ b/HTTPShortcuts/app/src/main/res/values/strings.xml
@@ -780,6 +780,8 @@
     Send UDP Packet
     
     Send TCP Packet
+    
+    Send MQTT Message
     
     Abort Execution