diff --git a/debian/control b/debian/control
index d56a6ac5..49ef4ffd 100644
--- a/debian/control
+++ b/debian/control
@@ -581,6 +581,15 @@ Description: nymea integration plugin for Tempo time tracking
This package contains the nymea integration plugin for Tempo time tracking.
+Package: nymea-plugin-tmate
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+ tmate,
+Description: nymea integration plugin for tmate
+ This package contains the nymea integration plugin for tmate terminal access.
+
+
Package: nymea-plugin-tplink
Architecture: any
Depends: ${shlibs:Depends},
diff --git a/debian/nymea-plugin-tmate.install.in b/debian/nymea-plugin-tmate.install.in
new file mode 100644
index 00000000..709018cb
--- /dev/null
+++ b/debian/nymea-plugin-tmate.install.in
@@ -0,0 +1,2 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugintmate.so
+tmate/translations/*qm usr/share/nymea/translations/
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index 969f2796..6e9da803 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -75,6 +75,7 @@ PLUGIN_DIRS = \
telegram \
tempo \
texasinstruments \
+ tmate \
tplink \
tuya \
udpcommander \
diff --git a/tmate/README.md b/tmate/README.md
new file mode 100644
index 00000000..d6bcfe38
--- /dev/null
+++ b/tmate/README.md
@@ -0,0 +1,26 @@
+# Tmate
+
+This plugin allows to establish a terminal connection to the device where nymea is running.
+
+This is useful when maintaining remote nymea setups which may be hidden behind a firewall
+and cannot be accessed from the public internet. A user can easily enable SSH access for
+a nymea setup by adding a Thing using the app without having to deal with DNS and NAT.
+
+## Setup
+
+A tmate thing can be created without any additional information. Leaving all the fields
+empty during setup will establish a default tmate session to tmate.io servers and generate
+a new random token. This is the equivalent of just running `tmate` in a terminal.
+
+For a more permanent solution, an API key can be registered at tmate.io. Using the api key a custom session name can be set which will be used for every connection establishment, persisting across nymea restarts. In the
+next step, provide the SSH credentials for the user on the SSH server which has been created before. Once the login succeeds, the thing should become connected.
+
+> Note: Using custom/self hosted tmate servers is not supported at this point.
+
+### Connecting clients
+
+The thing will inform about the session names via states and can be accessed via ssh or web.
+
+## More
+
+https://tmate.io/
diff --git a/tmate/integrationplugintmate.cpp b/tmate/integrationplugintmate.cpp
new file mode 100644
index 00000000..c7e0d006
--- /dev/null
+++ b/tmate/integrationplugintmate.cpp
@@ -0,0 +1,194 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2023, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "integrationplugintmate.h"
+#include "plugininfo.h"
+
+#include
+#include
+#include
+
+IntegrationPluginTmate::IntegrationPluginTmate()
+{
+
+}
+
+IntegrationPluginTmate::~IntegrationPluginTmate()
+{
+ foreach (QProcess *process, m_processes) {
+ process->terminate();
+ }
+}
+
+void IntegrationPluginTmate::setupThing(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+
+
+ QStringList arguments;
+ QString apiKey = thing->paramValue(tmateThingApiKeyParamTypeId).toString();
+ QString sessionName = thing->paramValue(tmateThingSessionNameParamTypeId).toString();
+
+ arguments << "-F";
+ if (!apiKey.isEmpty()) {
+ arguments << "-k" << apiKey;
+ if (!sessionName.isEmpty()) {
+ arguments << "-n" << sessionName;
+ arguments << "-r" << "ro-" + sessionName;
+ }
+ }
+ QProcess *process = new QProcess(thing);
+ process->setProgram("tmate");
+ process->setArguments(arguments);
+ process->setProcessChannelMode(QProcess::MergedChannels);
+
+ m_processes.insert(info->thing(), process);
+
+ connect(process, &QProcess::stateChanged, thing, [=](QProcess::ProcessState newState){
+ switch (newState) {
+ case QProcess::Starting:
+ qCDebug(dcTmate()) << "Connection starting for" << thing->name();
+ return ;
+ case QProcess::Running:
+ qCInfo(dcTmate()) << "Reverse SSH connected for" << thing->name();
+ thing->setStateValue(tmateConnectedStateTypeId, true);
+ return;
+ case QProcess::NotRunning:
+ qCInfo(dcTmate()) << "Reverse SSH disconnected for" << thing->name();
+ thing->setStateValue(tmateConnectedStateTypeId, false);
+ thing->setStateValue(tmateSshStateTypeId, "");
+ thing->setStateValue(tmateSshRoStateTypeId, "");
+ thing->setStateValue(tmateWebStateTypeId, "");
+ thing->setStateValue(tmateWebRoStateTypeId, "");
+ thing->setStateValue(tmateClientsStateTypeId, 0);
+ return;
+ }
+ });
+ connect(process, &QProcess::readyRead, thing, [=](){
+ while (process->canReadLine()) {
+ QByteArray data = process->readLine();
+
+ qCDebug(dcTmate()) << thing->name() << ":" << data;
+ auto extractSession = [thing](const StateTypeId &stateTypeId, const QString &type, const QString &input) {
+ int sessionStart = input.indexOf(type);
+ if (sessionStart >= 0) {
+ int sessionEnd = input.indexOf(QChar('\n'), sessionStart);
+ qCInfo(dcTmate()) << input << "Session start" << sessionStart << "session end" << sessionEnd;
+ QString session =input.mid(sessionStart + type.length(), sessionEnd);
+ thing->setStateValue(stateTypeId, session);
+ }
+ };
+
+ extractSession(tmateSshStateTypeId, "ssh session: ssh ", data);
+ extractSession(tmateSshRoStateTypeId, "ssh session read only: ssh ", data);
+ extractSession(tmateWebStateTypeId, "web session: ", data);
+ extractSession(tmateWebRoStateTypeId, "web session read only: ", data);
+
+ QRegularExpression joinAddressRegex("joined \\(([0-9\\.]+)\\)");
+ QRegularExpressionMatchIterator it = joinAddressRegex.globalMatch(data);
+ while (it.hasNext()) {
+ QRegularExpressionMatch match = it.next();
+ QString word = match.captured(1);
+ qCInfo(dcTmate()) << "Connected:" << word;
+ thing->emitEvent(tmateClientConnectedEventTypeId, {{tmateClientConnectedEventClientAddressParamTypeId, word}});
+ thing->setStateValue(tmateClientsStateTypeId, thing->stateValue(tmateClientsStateTypeId).toUInt()+1);
+ }
+ QRegularExpression leftAddressRegex("left \\(([0-9\\.]+)\\)");
+ it = leftAddressRegex.globalMatch(data);
+ while (it.hasNext()) {
+ QRegularExpressionMatch match = it.next();
+ QString word = match.captured(1);
+ qCInfo(dcTmate()) << "Disconnected:" << word;
+ thing->emitEvent(tmateClientDisconnectedEventTypeId, {{tmateClientDisconnectedEventClientAddressParamTypeId, word}});
+ thing->setStateValue(tmateClientsStateTypeId, thing->stateValue(tmateClientsStateTypeId).toUInt()-1);
+ }
+ }
+
+ });
+
+
+ // Start up now if enabled
+ bool enabled = thing->stateValue(tmateActiveStateTypeId).toBool();
+ if (enabled) {
+ process->start();
+ }
+
+ info->finish(Thing::ThingErrorNoError);
+
+
+ // Create a watchdog to reconnect if a connection drops...
+ if (!m_watchdog) {
+ m_watchdog = hardwareManager()->pluginTimerManager()->registerTimer(10);
+ connect(m_watchdog, &PluginTimer::timeout, this, [this](){
+ foreach (Thing *thing, m_processes.keys()) {
+ QProcess *process = m_processes.value(thing);
+ if (thing->stateValue(tmateActiveStateTypeId).toBool() && process->state() == QProcess::NotRunning) {
+ qCInfo(dcTmate()) << "Reconnecting tmate for" << thing->name();
+ process->start();
+ }
+ }
+ });
+ }
+}
+
+
+void IntegrationPluginTmate::thingRemoved(Thing *thing)
+{
+ if (thing->thingClassId() == tmateThingClassId) {
+ QProcess *process = m_processes.take(thing);
+ if (process->state() != QProcess::NotRunning) {
+ process->terminate();
+ process->waitForFinished();
+ }
+ }
+
+ if (myThings().isEmpty()) {
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_watchdog);
+ m_watchdog = nullptr;
+ }
+}
+
+
+void IntegrationPluginTmate::executeAction(ThingActionInfo *info)
+{
+ if (info->action().actionTypeId() == tmateActiveActionTypeId) {
+ bool active = info->action().paramValue(tmateActiveActionActiveParamTypeId).toBool();
+ QProcess *process = m_processes.value(info->thing());
+ if (active) {
+ qCInfo(dcTmate()) << "Reconnecting tmate for" << info->thing()->name();
+ process->start();
+ } else {
+ qCInfo(dcTmate()) << "Terminating session for" << info->thing()->name();
+ process->terminate();
+ }
+ info->thing()->setStateValue(tmateActiveStateTypeId, active);
+ info->finish(Thing::ThingErrorNoError);
+ }
+}
diff --git a/tmate/integrationplugintmate.h b/tmate/integrationplugintmate.h
new file mode 100644
index 00000000..b308445e
--- /dev/null
+++ b/tmate/integrationplugintmate.h
@@ -0,0 +1,62 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2023, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef INTEGRATIONPLUGINREMOTESSH_H
+#define INTEGRATIONPLUGINREMOTESSH_H
+
+#include "plugintimer.h"
+#include "integrations/integrationplugin.h"
+#include "extern-plugininfo.h"
+
+#include
+
+class IntegrationPluginTmate : public IntegrationPlugin
+{
+ Q_OBJECT
+
+ Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugintmate.json")
+ Q_INTERFACES(IntegrationPlugin)
+
+public:
+ explicit IntegrationPluginTmate();
+ ~IntegrationPluginTmate();
+
+// void startPairing(ThingPairingInfo *info) override;
+// void confirmPairing(ThingPairingInfo *info, const QString &user, const QString &secret) override;
+ void executeAction(ThingActionInfo *info) override;
+ void setupThing(ThingSetupInfo *info) override;
+ void thingRemoved(Thing *thing) override;
+
+private:
+ QHash m_processes;
+ PluginTimer *m_watchdog = nullptr;
+};
+
+#endif // INTEGRATIONPLUGINREMOTESSH_H
diff --git a/tmate/integrationplugintmate.json b/tmate/integrationplugintmate.json
new file mode 100644
index 00000000..5329448a
--- /dev/null
+++ b/tmate/integrationplugintmate.json
@@ -0,0 +1,131 @@
+{
+ "id": "d06ab0d1-dbfe-48af-b196-523cc37a1e5e",
+ "name": "Tmate",
+ "displayName": "Tmate",
+ "vendors": [
+ {
+ "id": "b948d5e2-bfc6-4e28-a2ba-e40e46f4c213",
+ "name": "tmate",
+ "displayName": "Tmate",
+ "thingClasses": [
+ {
+ "id": "3f06ad52-9514-41b1-9bf9-031241d34634",
+ "name": "tmate",
+ "displayName": "Tmate",
+ "createMethods": ["user"],
+ "setupMethod": "justadd",
+ "interfaces": [],
+ "paramTypes": [
+ {
+ "id": "01f0c818-55e1-4842-a9b9-cc58bbfe76c6",
+ "name": "apiKey",
+ "displayName": "API key (optional)",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "e587e3dc-0beb-441f-8f07-b23c25580b10",
+ "name": "sessionName",
+ "displayName": "Session name (requires API key usage)",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ],
+ "stateTypes":[
+ {
+ "id": "7009c176-e1aa-49bc-818c-63f7a9027306",
+ "name": "active",
+ "displayName": "Active",
+ "displayNameAction": "Set active",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "id": "beac3113-04f1-4d70-875b-44ca8b307866",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "suggestLogging": true,
+ "cached": false
+ },
+ {
+ "id": "ef780fb1-b31a-4333-944b-a02bf3297fea",
+ "name": "sshRo",
+ "displayName": "SSH RO",
+ "type": "QString",
+ "defaultValue": "",
+ "cached": false
+ },
+ {
+ "id": "e8ff1b90-7701-454c-a557-4b91dc8c649b",
+ "name": "ssh",
+ "displayName": "SSH",
+ "type": "QString",
+ "defaultValue": "",
+ "cached": false
+ },
+ {
+ "id": "98248bc0-ddda-4ae6-8558-4d7155a39c33",
+ "name": "webRo",
+ "displayName": "Web RO",
+ "type": "QString",
+ "defaultValue": "",
+ "cached": false
+ },
+ {
+ "id": "9c284ede-eea8-4a9b-a326-e59a6bc7bb7c",
+ "name": "web",
+ "displayName": "Web",
+ "type": "QString",
+ "defaultValue": "",
+ "cached": false
+ },
+ {
+ "id": "786e7be7-917a-4062-83ff-aade80686ec5",
+ "name": "clients",
+ "displayName": "Clients",
+ "type": "uint",
+ "defaultValue": 0,
+ "cached": false
+ }
+ ],
+ "eventTypes": [
+ {
+ "id": "0508a1e2-4ed2-42ee-ab70-ed7cdd1e261c",
+ "name": "clientConnected",
+ "displayName": "Client connected",
+ "suggestLogging": true,
+ "paramTypes": [
+ {
+ "id": "a334c6e7-dffc-4720-aa21-815636be1bc1",
+ "name": "clientAddress",
+ "displayName": "Client address",
+ "type": "QString"
+ }
+ ]
+ },
+ {
+ "id": "2871e481-1b67-4d77-b1ce-e0965784aa89",
+ "name": "clientDisconnected",
+ "displayName": "Client disconnected",
+ "suggestLogging": true,
+ "paramTypes": [
+ {
+ "id": "0ad4ed71-4b9a-44ab-83b1-5c62482e1625",
+ "name": "clientAddress",
+ "displayName": "Client address",
+ "type": "QString"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+
+
+
diff --git a/tmate/logo.png b/tmate/logo.png
new file mode 100644
index 00000000..0872a0dc
Binary files /dev/null and b/tmate/logo.png differ
diff --git a/tmate/meta.json b/tmate/meta.json
new file mode 100644
index 00000000..3089444d
--- /dev/null
+++ b/tmate/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "Tmate",
+ "tagline": "Shell access to the machine running nymea from anywhere via tmate.",
+ "icon": "logo.png",
+ "stability": "community",
+ "offline": false,
+ "technologies": [
+ "cloud"
+ ],
+ "categories": [
+ "tool"
+ ]
+}
diff --git a/tmate/tmate.pro b/tmate/tmate.pro
new file mode 100644
index 00000000..b53656b6
--- /dev/null
+++ b/tmate/tmate.pro
@@ -0,0 +1,8 @@
+include(../plugins.pri)
+
+SOURCES += \
+ integrationplugintmate.cpp \
+
+HEADERS += \
+ integrationplugintmate.h \
+
diff --git a/tmate/translations/d06ab0d1-dbfe-48af-b196-523cc37a1e5e-en_US.ts b/tmate/translations/d06ab0d1-dbfe-48af-b196-523cc37a1e5e-en_US.ts
new file mode 100644
index 00000000..e7936aff
--- /dev/null
+++ b/tmate/translations/d06ab0d1-dbfe-48af-b196-523cc37a1e5e-en_US.ts
@@ -0,0 +1,103 @@
+
+
+
+
+ Tmate
+
+
+ API key (optional)
+ The name of the ParamType (ThingClass: tmate, Type: thing, ID: {01f0c818-55e1-4842-a9b9-cc58bbfe76c6})
+
+
+
+
+
+ Active
+ The name of the ParamType (ThingClass: tmate, ActionType: active, ID: {7009c176-e1aa-49bc-818c-63f7a9027306})
+----------
+The name of the StateType ({7009c176-e1aa-49bc-818c-63f7a9027306}) of ThingClass tmate
+
+
+
+
+
+ Client address
+ The name of the ParamType (ThingClass: tmate, EventType: clientDisconnected, ID: {0ad4ed71-4b9a-44ab-83b1-5c62482e1625})
+----------
+The name of the ParamType (ThingClass: tmate, EventType: clientConnected, ID: {a334c6e7-dffc-4720-aa21-815636be1bc1})
+
+
+
+
+ Client connected
+ The name of the EventType ({0508a1e2-4ed2-42ee-ab70-ed7cdd1e261c}) of ThingClass tmate
+
+
+
+
+ Client disconnected
+ The name of the EventType ({2871e481-1b67-4d77-b1ce-e0965784aa89}) of ThingClass tmate
+
+
+
+
+ Clients
+ The name of the StateType ({786e7be7-917a-4062-83ff-aade80686ec5}) of ThingClass tmate
+
+
+
+
+ Connected
+ The name of the StateType ({beac3113-04f1-4d70-875b-44ca8b307866}) of ThingClass tmate
+
+
+
+
+ SSH
+ The name of the StateType ({e8ff1b90-7701-454c-a557-4b91dc8c649b}) of ThingClass tmate
+
+
+
+
+ SSH RO
+ The name of the StateType ({ef780fb1-b31a-4333-944b-a02bf3297fea}) of ThingClass tmate
+
+
+
+
+ Session name (requires API key usage)
+ The name of the ParamType (ThingClass: tmate, Type: thing, ID: {e587e3dc-0beb-441f-8f07-b23c25580b10})
+
+
+
+
+ Set active
+ The name of the ActionType ({7009c176-e1aa-49bc-818c-63f7a9027306}) of ThingClass tmate
+
+
+
+
+
+
+ Tmate
+ The name of the ThingClass ({3f06ad52-9514-41b1-9bf9-031241d34634})
+----------
+The name of the vendor ({b948d5e2-bfc6-4e28-a2ba-e40e46f4c213})
+----------
+The name of the plugin Tmate ({d06ab0d1-dbfe-48af-b196-523cc37a1e5e})
+
+
+
+
+ Web
+ The name of the StateType ({9c284ede-eea8-4a9b-a326-e59a6bc7bb7c}) of ThingClass tmate
+
+
+
+
+ Web RO
+ The name of the StateType ({98248bc0-ddda-4ae6-8558-4d7155a39c33}) of ThingClass tmate
+
+
+
+