diff --git a/debian/control b/debian/control
index e894e2a7..244651b5 100644
--- a/debian/control
+++ b/debian/control
@@ -214,6 +214,15 @@ Description: nymea integration plugin for ESPuino
This package contains the nymea integration plugin for ESPuino devices.
+Package: nymea-plugin-espsomfyrts
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+Conflicts: nymea-plugins-translations (<< 1.0.1)
+Description: nymea integration plugin for ESP-Somfy-RTS
+ This package contains the nymea integration plugin for the ESP Somfy RTS project.
+
+
Package: nymea-plugin-evbox
Architecture: any
Depends: ${shlibs:Depends},
diff --git a/debian/nymea-plugin-espsomfyrts.install.in b/debian/nymea-plugin-espsomfyrts.install.in
new file mode 100644
index 00000000..cf7c6015
--- /dev/null
+++ b/debian/nymea-plugin-espsomfyrts.install.in
@@ -0,0 +1,2 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginespsomfyrts.so
+espsomfyrts/translations/*qm usr/share/nymea/translations/
diff --git a/espsomfyrts/README.md b/espsomfyrts/README.md
new file mode 100644
index 00000000..02bacf87
--- /dev/null
+++ b/espsomfyrts/README.md
@@ -0,0 +1,7 @@
+# ESPSomfy-RTS
+
+This integration adds support for Somfy RTS devices using the ESPSomfy-RTS project.
+
+For more information about the project and how to build your own somfy controller under 12$ have look on the project page.
+
+https://github.com/rstrouse/ESPSomfy-RTS
\ No newline at end of file
diff --git a/espsomfyrts/espsomfy-rts.png b/espsomfyrts/espsomfy-rts.png
new file mode 100644
index 00000000..d16fc347
Binary files /dev/null and b/espsomfyrts/espsomfy-rts.png differ
diff --git a/espsomfyrts/espsomfyrts.cpp b/espsomfyrts/espsomfyrts.cpp
new file mode 100644
index 00000000..caa8d2f5
--- /dev/null
+++ b/espsomfyrts/espsomfyrts.cpp
@@ -0,0 +1,249 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, 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 "espsomfyrts.h"
+#include "extern-plugininfo.h"
+
+#include
+
+#include
+#include
+
+EspSomfyRts::EspSomfyRts(NetworkDeviceMonitor *monitor, QObject *parent)
+ : QObject{parent},
+ m_monitor{monitor}
+{
+ m_websocketUrl.setScheme("ws");
+ m_websocketUrl.setHost("127.0.0.1");
+ m_websocketUrl.setPort(8080);
+
+ m_webSocket = new QWebSocket("nymea", QWebSocketProtocol::Version13, this);
+ connect(m_webSocket, &QWebSocket::textMessageReceived, this, &EspSomfyRts::onWebSocketTextMessageReceived);
+ connect(m_webSocket, &QWebSocket::connected, this, [this](){
+ qCDebug(dcESPSomfyRTS()) << "Websocket connected";
+ m_connected = true;
+ emit connectedChanged(m_connected);
+ });
+
+ connect(m_webSocket, &QWebSocket::disconnected, this, [this](){
+ qCDebug(dcESPSomfyRTS()) << "Websocket disconnected";
+ m_connected = false;
+ emit connectedChanged(m_connected);
+
+ m_reconnectTimer.start();
+ });
+
+ if (m_monitor) {
+ qCDebug(dcESPSomfyRTS()) << "Setting up ESP Somfy using the network device monitor on" << m_monitor->macAddress();
+ connect(m_monitor, &NetworkDeviceMonitor::reachableChanged, this, &EspSomfyRts::onMonitorReachableChanged);
+
+ // Init connection based on the monitor
+ onMonitorReachableChanged(m_monitor->reachable());
+ }
+
+ // Websocket reconnect mechanism
+ m_reconnectTimer.setInterval(5);
+ m_reconnectTimer.setSingleShot(false);
+ connect(&m_reconnectTimer, &QTimer::timeout, this, [this](){
+ if (m_webSocket->state() == QAbstractSocket::UnconnectedState && m_monitor->reachable()) {
+ m_websocketUrl.setHost(m_monitor->networkDeviceInfo().address().toString());
+ qCDebug(dcESPSomfyRTS()) << "Trying to connect to" << m_websocketUrl;
+ m_webSocket->open(m_websocketUrl);
+ }
+ });
+}
+
+QHostAddress EspSomfyRts::address() const
+{
+ return QHostAddress(m_websocketUrl.host());
+}
+
+bool EspSomfyRts::connected() const
+{
+ return m_connected;
+}
+
+QString EspSomfyRts::firmwareVersion() const
+{
+ return m_firmwareVersion;
+}
+
+QUrl EspSomfyRts::shadesUrl()
+{
+ return buildUrl("shades");
+}
+
+QUrl EspSomfyRts::shadeCommandUrl()
+{
+ return buildUrl("shadeCommand");
+}
+
+QUrl EspSomfyRts::tiltCommandUrl()
+{
+ return buildUrl("tiltCommand");
+}
+
+QString EspSomfyRts::getShadeCommandString(ShadeCommand shadeCommand)
+{
+ QString shadeCommandString;
+
+ switch(shadeCommand) {
+ case ShadeCommandMy:
+ shadeCommandString = "m";
+ break;
+ case ShadeCommandUp:
+ shadeCommandString = "u";
+ break;
+ case ShadeCommandDown:
+ shadeCommandString = "d";
+ break;
+ case ShadeCommandMyUp:
+ shadeCommandString = "mu";
+ break;
+ case ShadeCommandMyDown:
+ shadeCommandString = "md";
+ break;
+ case ShadeCommandUpDown:
+ shadeCommandString = "ud";
+ break;
+ case ShadeCommandMyUpDown:
+ shadeCommandString = "mud";
+ break;
+ case ShadeCommandProg:
+ shadeCommandString = "p";
+ break;
+ case ShadeCommandSunFlag:
+ shadeCommandString = "s";
+ break;
+ case ShadeCommandFlag:
+ shadeCommandString = "f";
+ break;
+ case ShadeCommandStepUp:
+ shadeCommandString = "su";
+ break;
+ case ShadeCommandStepDown:
+ shadeCommandString = "sd";
+ break;
+ case ShadeCommandFavorite:
+ shadeCommandString = "fav";
+ break;
+ case ShadeCommandStop:
+ shadeCommandString = "stop";
+ break;
+ }
+
+ return shadeCommandString;
+}
+
+void EspSomfyRts::onMonitorReachableChanged(bool reachable)
+{
+ qCDebug(dcESPSomfyRTS()) << "Network device of" << m_websocketUrl.host() << "is" << (reachable ? "now reachable" : "not reachable any more");
+
+ if (reachable) {
+ if (m_webSocket->state() == QAbstractSocket::ConnectedState)
+ return;
+
+ m_websocketUrl.setHost(m_monitor->networkDeviceInfo().address().toString());
+ qCDebug(dcESPSomfyRTS()) << "Connecting to" << m_websocketUrl.toString();
+ m_webSocket->open(m_websocketUrl);
+ }
+}
+
+void EspSomfyRts::onWebSocketTextMessageReceived(const QString &message)
+{
+ //qCDebug(dcESPSomfyRTS()) << "Websocket message received:" << message;
+
+ if (message.startsWith("42")) {
+ QJsonParseError jsonError;
+ QByteArray rawMessage = message.mid(3, message.size() - 4).toUtf8();
+ // Make parsing easier
+ int index = rawMessage.indexOf(',');
+ if (index < 0) {
+ qCWarning(dcESPSomfyRTS()) << "Could not parse notification from data" << rawMessage;
+ return;
+ }
+
+ QString notification = rawMessage.left(index);
+ QByteArray rawPayload = rawMessage.right(rawMessage.size() - index - 1);
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(rawPayload, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError) {
+ qCWarning(dcESPSomfyRTS()) << "Json error parsing the data" << rawPayload << jsonError.error << jsonError.errorString();
+ return;
+ }
+
+ QVariantMap payload = jsonDoc.toVariant().toMap();
+
+ if (notification == "wifiStrength") {
+
+ uint signalStrength = 0;
+ int dbm = payload.value("strength").toInt();
+ if (dbm > -90)
+ signalStrength += 20;
+ if (dbm > -80)
+ signalStrength += 20;
+ if (dbm > -70)
+ signalStrength += 20;
+ if (dbm > -67)
+ signalStrength += 20;
+ if (dbm > -30)
+ signalStrength += 20;
+
+ if (m_signalStrength != signalStrength) {
+ m_signalStrength = signalStrength;
+ emit signalStrengthChanged(m_signalStrength);
+ }
+
+ } else if (notification == "fwStatus") {
+
+ QString firmwareVersion = payload.value("fwVersion").toMap().value("name").toString();
+ if (m_firmwareVersion != firmwareVersion) {
+ m_firmwareVersion = firmwareVersion;
+ emit firmwareVersionChanged(m_firmwareVersion);
+ }
+
+ // TODO. firmware update
+
+ } else if (notification == "shadeState") {
+ emit shadeStateReceived(payload);
+ } else if (notification == "memStatus") {
+ // We are not interested in this, filter it out
+ } else {
+ qCDebug(dcESPSomfyRTS()) << "Notification" << notification << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
+ }
+
+ }
+}
+
+QUrl EspSomfyRts::buildUrl(const QString &path)
+{
+ return QUrl(QString("http://%1/%2").arg(m_websocketUrl.host()).arg(path));
+}
+
diff --git a/espsomfyrts/espsomfyrts.h b/espsomfyrts/espsomfyrts.h
new file mode 100644
index 00000000..2820b200
--- /dev/null
+++ b/espsomfyrts/espsomfyrts.h
@@ -0,0 +1,135 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, 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 ESPSOMFYRTS_H
+#define ESPSOMFYRTS_H
+
+#include
+#include
+#include
+#include
+
+class NetworkDeviceMonitor;
+
+class EspSomfyRts : public QObject
+{
+ Q_OBJECT
+public:
+ enum ShadeType {
+ ShadeTypeRollerShade = 0,
+ ShadeTypeBlind = 1,
+ ShadeTypeDrapery = 2,
+ ShadeTypeAwning = 3,
+ ShadeTypeShutter = 4
+ };
+ Q_ENUM(ShadeType)
+
+ enum MovingDirection {
+ MovingDirectionUp = -1,
+ MovingDirectionRest = 0,
+ MovingDirectionDown = 1
+ };
+ Q_ENUM(MovingDirection)
+
+ enum TileType {
+ TileTypeNone = 0,
+ TileTypeSeparateTileMotor = 1,
+ TileTypeIntegratedTileMechanism = 2,
+ TileTypeTileOnly = 3
+ };
+ Q_ENUM(TileType)
+
+ enum SensorFlag {
+ SensorFlagSunOn = 0x01,
+ SensorFlagDemoMode = 0x04,
+ SensorFlagWindy = 0x10,
+ SensorFlagSuny = 0x20,
+ };
+ Q_DECLARE_FLAGS(SensorFlags, SensorFlag)
+ Q_FLAG(SensorFlags)
+
+ enum ShadeCommand {
+ ShadeCommandMy,
+ ShadeCommandUp,
+ ShadeCommandDown,
+ ShadeCommandMyUp,
+ ShadeCommandMyDown,
+ ShadeCommandUpDown,
+ ShadeCommandMyUpDown,
+ ShadeCommandProg,
+ ShadeCommandSunFlag, // Turns the sun sensor on
+ ShadeCommandFlag, // Turns the sun sensor off
+ ShadeCommandStepUp,
+ ShadeCommandStepDown,
+ ShadeCommandFavorite,
+ ShadeCommandStop
+ };
+ Q_ENUM(ShadeCommand)
+
+ explicit EspSomfyRts(NetworkDeviceMonitor *monitor, QObject *parent = nullptr);
+
+ QHostAddress address() const;
+
+ bool connected() const;
+ uint signalStrength() const;
+ QString firmwareVersion() const;
+
+
+ QUrl shadesUrl();
+ QUrl shadeCommandUrl();
+ QUrl tiltCommandUrl();
+
+ static QString getShadeCommandString(ShadeCommand shadeCommand);
+
+signals:
+ void connectedChanged(bool connected);
+ void signalStrengthChanged(uint signalStrength);
+ void firmwareVersionChanged(const QString &firmwareVersion);
+ void shadeStateReceived(const QVariantMap &shadeState);
+
+private slots:
+ void onMonitorReachableChanged(bool reachable);
+ void onWebSocketTextMessageReceived(const QString &message);
+
+private:
+ NetworkDeviceMonitor *m_monitor = nullptr;
+
+ QUrl m_websocketUrl;
+ QWebSocket *m_webSocket = nullptr;
+ QTimer m_reconnectTimer;
+
+ bool m_connected = false;
+ uint m_signalStrength = 0;
+ QString m_firmwareVersion;
+
+ QUrl buildUrl(const QString &path);
+};
+
+#endif // ESPSOMFYRTS_H
diff --git a/espsomfyrts/espsomfyrts.pro b/espsomfyrts/espsomfyrts.pro
new file mode 100644
index 00000000..e0e85d66
--- /dev/null
+++ b/espsomfyrts/espsomfyrts.pro
@@ -0,0 +1,13 @@
+include(../plugins.pri)
+
+QT += network websockets
+
+SOURCES += \
+ espsomfyrts.cpp \
+ espsomfyrtsdiscovery.cpp \
+ integrationpluginespsomfyrts.cpp \
+
+HEADERS += \
+ espsomfyrts.h \
+ espsomfyrtsdiscovery.h \
+ integrationpluginespsomfyrts.h \
diff --git a/espsomfyrts/espsomfyrtsdiscovery.cpp b/espsomfyrts/espsomfyrtsdiscovery.cpp
new file mode 100644
index 00000000..3bcf7481
--- /dev/null
+++ b/espsomfyrts/espsomfyrtsdiscovery.cpp
@@ -0,0 +1,118 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, 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 "espsomfyrtsdiscovery.h"
+#include "extern-plugininfo.h"
+
+#include
+#include
+
+EspSomfyRtsDiscovery::EspSomfyRtsDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) :
+ QObject{parent},
+ m_networkManager{networkManager},
+ m_networkDeviceDiscovery{networkDeviceDiscovery}
+{
+ m_gracePeriodTimer.setSingleShot(true);
+ m_gracePeriodTimer.setInterval(3000);
+ connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){
+ finishDiscovery();
+ });
+}
+
+void EspSomfyRtsDiscovery::startDiscovery()
+{
+ qCDebug(dcESPSomfyRTS()) << "Discovery: Searching for Fronius solar devices in the network...";
+ m_startDateTime = QDateTime::currentDateTime();
+
+ NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::networkDeviceInfoAdded, this, &EspSomfyRtsDiscovery::checkNetworkDevice);
+ connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
+ qCDebug(dcESPSomfyRTS()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
+ m_gracePeriodTimer.start();
+ discoveryReply->deleteLater();
+ });
+}
+
+QList EspSomfyRtsDiscovery::results() const
+{
+ return m_results;
+}
+
+void EspSomfyRtsDiscovery::checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo)
+{
+ qCDebug(dcESPSomfyRTS()) << "Discovery: Verifying" << networkDeviceInfo;
+ QUrl url;
+ url.setScheme("http");
+ url.setHost(networkDeviceInfo.address().toString());
+ url.setPort(8081);
+ url.setPath("/discovery");
+
+ QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [this, reply, networkDeviceInfo](){
+ if (reply->error() != QNetworkReply::NoError) {
+ qCDebug(dcESPSomfyRTS()) << "Discovery: Reply finished with error" << reply->errorString() << "Continue...";
+ return;
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError);
+
+ if (jsonError.error != QJsonParseError::NoError) {
+ qCDebug(dcESPSomfyRTS()) << "Discovery: Reply contains invalid JSON data" << jsonError.errorString() << "Continue...";
+ return;
+ }
+
+ QVariantMap responseMap = jsonDoc.toVariant().toMap();
+ if (responseMap.contains("model") && responseMap.value("model").toString().toLower().contains("espsomfyrts")) {
+
+ Result result;
+ result.networkDeviceInfo = networkDeviceInfo;
+ result.name = responseMap.value("serverId").toString();
+ result.firmwareVersion = responseMap.value("version").toString();
+ m_results.append(result);
+
+ qCDebug(dcESPSomfyRTS()) << "Discovery: --> Found ESPSomfy-RTS device" << result.name << result.firmwareVersion
+ << "on" << result.networkDeviceInfo.address().toString() ;
+ }
+ });
+}
+
+void EspSomfyRtsDiscovery::finishDiscovery()
+{
+ qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
+ qCDebug(dcESPSomfyRTS()) << "Discovery: Finished the discovery process. Found" << m_results.count()
+ << "ESPSomfy-RTS devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
+ m_gracePeriodTimer.stop();
+
+ emit discoveryFinished();
+}
+
diff --git a/espsomfyrts/espsomfyrtsdiscovery.h b/espsomfyrts/espsomfyrtsdiscovery.h
new file mode 100644
index 00000000..8b69c196
--- /dev/null
+++ b/espsomfyrts/espsomfyrtsdiscovery.h
@@ -0,0 +1,73 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, 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 ESPSOMFYRTSDISCOVERY_H
+#define ESPSOMFYRTSDISCOVERY_H
+
+#include
+#include
+
+#include
+#include
+
+class EspSomfyRtsDiscovery : public QObject
+{
+ Q_OBJECT
+public:
+ explicit EspSomfyRtsDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr);
+
+ typedef struct Result {
+ QString name;
+ QString firmwareVersion;
+ NetworkDeviceInfo networkDeviceInfo;
+ } Result;
+
+ void startDiscovery();
+
+ QList results() const;
+
+signals:
+ void discoveryFinished();
+
+private:
+ NetworkAccessManager *m_networkManager = nullptr;
+ NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
+
+ QTimer m_gracePeriodTimer;
+ QDateTime m_startDateTime;
+
+ QList m_results;
+
+ void checkNetworkDevice(const NetworkDeviceInfo &networkDeviceInfo);
+
+ void finishDiscovery();
+};
+
+#endif // ESPSOMFYRTSDISCOVERY_H
diff --git a/espsomfyrts/integrationpluginespsomfyrts.cpp b/espsomfyrts/integrationpluginespsomfyrts.cpp
new file mode 100644
index 00000000..cd77d48a
--- /dev/null
+++ b/espsomfyrts/integrationpluginespsomfyrts.cpp
@@ -0,0 +1,438 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, 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 "integrationpluginespsomfyrts.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include "plugininfo.h"
+#include "espsomfyrtsdiscovery.h"
+
+IntegrationPluginEspSomfyRts::IntegrationPluginEspSomfyRts()
+{
+
+}
+
+void IntegrationPluginEspSomfyRts::init()
+{
+
+}
+
+void IntegrationPluginEspSomfyRts::discoverThings(ThingDiscoveryInfo *info)
+{
+ if (!hardwareManager()->networkDeviceDiscovery()->available()) {
+ qCWarning(dcESPSomfyRTS()) << "Failed to discover network devices. The network device discovery is not available.";
+ info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network."));
+ return;
+ }
+
+ qCInfo(dcESPSomfyRTS()) << "Starting network discovery...";
+ EspSomfyRtsDiscovery *discovery = new EspSomfyRtsDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info);
+ connect(discovery, &EspSomfyRtsDiscovery::discoveryFinished, info, [=](){
+ ThingDescriptors descriptors;
+ qCInfo(dcESPSomfyRTS()) << "Discovery finished. Found" << discovery->results().count() << "devices";
+ foreach (const EspSomfyRtsDiscovery::Result &result, discovery->results()) {
+ qCInfo(dcESPSomfyRTS()) << "Discovered device on" << result.networkDeviceInfo;
+ if (result.networkDeviceInfo.macAddress().isNull())
+ continue;
+
+ QString title = "ESP Somfy RTS (" + result.name + ")";
+ QString description = result.networkDeviceInfo.address().toString() + " (" + result.networkDeviceInfo.macAddress() + ")";
+
+ ThingDescriptor descriptor(espSomfyRtsThingClassId, title, description);
+
+ // Check if we already have set up this device
+ Things existingThings = myThings().filterByParam(espSomfyRtsThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
+ if (existingThings.count() == 1) {
+ qCDebug(dcESPSomfyRTS()) << "This thing already exists in the system." << existingThings.first() << result.networkDeviceInfo;
+ descriptor.setThingId(existingThings.first()->id());
+ }
+
+ ParamList params;
+ params << Param(espSomfyRtsThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress());
+ descriptor.setParams(params);
+ info->addThingDescriptor(descriptor);
+ }
+
+ info->finish(Thing::ThingErrorNoError);
+ });
+
+ discovery->startDiscovery();
+}
+
+void IntegrationPluginEspSomfyRts::setupThing(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+
+ if (thing->thingClassId() == espSomfyRtsThingClassId) {
+ if (!hardwareManager()->networkDeviceDiscovery()->available()) {
+ qCWarning(dcESPSomfyRTS()) << "Cannot set up thing because the network discovery is not available.";
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ MacAddress macAddress(thing->paramValue(espSomfyRtsThingMacAddressParamTypeId).toString());
+ if (!macAddress.isValid()) {
+ qCWarning(dcESPSomfyRTS()) << "Invalid MAC address, cannot set up thing" << thing << thing->params();
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(macAddress);
+
+ EspSomfyRts *somfy = new EspSomfyRts(monitor, thing);
+ m_somfys.insert(thing, somfy);
+
+ connect(somfy, &EspSomfyRts::connectedChanged, thing, [this, thing](bool connected){
+ onEspSomfyConnectedChanged(thing, connected);
+ });
+
+ connect(somfy, &EspSomfyRts::signalStrengthChanged, thing, [thing](uint signalStrength){
+ thing->setStateValue(espSomfyRtsSignalStrengthStateTypeId, signalStrength);
+ });
+
+ connect(somfy, &EspSomfyRts::firmwareVersionChanged, thing, [thing](const QString &firmwareVersion){
+ thing->setStateValue(espSomfyRtsFirmwareVersionStateTypeId, firmwareVersion);
+ });
+
+ connect(somfy, &EspSomfyRts::shadeStateReceived, thing, [this](const QVariantMap &shadeState){
+ int shadeId = shadeState.value("shadeId").toInt();
+ if (m_shadeThings.contains(shadeId)) {
+ processShadeState(m_shadeThings.value(shadeId), shadeState);
+ }
+ });
+
+ info->finish(Thing::ThingErrorNoError);
+ return;
+ } else {
+ qCDebug(dcESPSomfyRTS()) << "Setting up" << thing->thingClass().name() << thing->name();
+ m_shadeThings.insert(thing->paramValue("shadeId").toUInt(), thing);
+ info->finish(Thing::ThingErrorNoError);
+ }
+}
+
+void IntegrationPluginEspSomfyRts::postSetupThing(Thing *thing)
+{
+ if (thing->thingClassId() == espSomfyRtsThingClassId) {
+ EspSomfyRts *somfy = m_somfys.value(thing);
+ onEspSomfyConnectedChanged(thing, somfy->connected());
+
+ if (!m_refreshTimer) {
+ m_refreshTimer = hardwareManager()->pluginTimerManager()->registerTimer(60);
+ connect(m_refreshTimer, &PluginTimer::timeout, thing, [this, thing](){
+ if (m_somfys.value(thing)->connected()) {
+ synchronizeShades(thing);
+ }
+ });
+ }
+
+ } else {
+ Thing *parent = myThings().findById(thing->parentId());
+ EspSomfyRts *somfy = m_somfys.value(parent);
+ if (!parent || !somfy)
+ return;
+
+ thing->setStateValue("connected", somfy->connected());
+ }
+}
+
+void IntegrationPluginEspSomfyRts::thingRemoved(Thing *thing)
+{
+ Q_UNUSED(thing)
+}
+
+void IntegrationPluginEspSomfyRts::executeAction(ThingActionInfo *info)
+{
+ Thing *thing = info->thing();
+ Action action = info->action();
+
+ if (thing->thingClassId() == awningThingClassId) {
+
+ if (!thing->stateValue(awningConnectedStateTypeId).toBool()) {
+ qCWarning(dcESPSomfyRTS()) << "Could not execute command because the thing is not connected" << thing;
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ Thing *parentThing = myThings().findById(thing->parentId());
+ EspSomfyRts *somfy = m_somfys.value(parentThing);
+ if (!parentThing || !somfy) {
+ qCWarning(dcESPSomfyRTS()) << "Could not execute command because the parent thing could not be found for" << thing;
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ QVariantMap requestMap;
+ requestMap.insert("shadeId", thing->paramValue(awningThingShadeIdParamTypeId).toUInt());
+
+ if (action.actionTypeId() == awningOpenActionTypeId) {
+ requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandDown));
+ } else if (action.actionTypeId() == awningStopActionTypeId) {
+ requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandMy));
+ } else if (action.actionTypeId() == awningCloseActionTypeId) {
+ requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandUp));
+ } else if (action.actionTypeId() == awningPercentageActionTypeId) {
+ requestMap.insert("target", action.paramValue(awningPercentageActionPercentageParamTypeId).toUInt());
+ }
+
+ QNetworkRequest request(somfy->shadeCommandUrl());
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ QNetworkReply *reply = hardwareManager()->networkManager()->put(request, QJsonDocument::fromVariant(requestMap).toJson());
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, info, [reply, info](){
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qCWarning(dcESPSomfyRTS()) << "Could not execute command on" << info->thing() << "because the network request finished with error" << reply->errorString();
+ info->finish(Thing::ThingErrorHardwareFailure);
+ return;
+ }
+
+ qCDebug(dcESPSomfyRTS()) << "Executed command successfully on" << info->thing();
+ info->finish(Thing::ThingErrorNoError);
+ });
+
+ return;
+ }
+
+ if (thing->thingClassId() == venetianBlindThingClassId) {
+
+ if (!thing->stateValue(venetianBlindConnectedStateTypeId).toBool()) {
+ qCWarning(dcESPSomfyRTS()) << "Could not execute command because the thing is not connected" << thing;
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ Thing *parentThing = myThings().findById(thing->parentId());
+ EspSomfyRts *somfy = m_somfys.value(parentThing);
+ if (!parentThing || !somfy) {
+ qCWarning(dcESPSomfyRTS()) << "Could not execute command because the parent thing could not be found for" << thing;
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ return;
+ }
+
+ QVariantMap requestMap;
+ requestMap.insert("shadeId", thing->paramValue(venetianBlindThingShadeIdParamTypeId).toUInt());
+
+ QUrl url = somfy->shadeCommandUrl();
+ if (action.actionTypeId() == venetianBlindOpenActionTypeId) {
+ requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandUp));
+ } else if (action.actionTypeId() == venetianBlindStopActionTypeId) {
+ requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandMy));
+ } else if (action.actionTypeId() == venetianBlindCloseActionTypeId) {
+ requestMap.insert("command", EspSomfyRts::getShadeCommandString(EspSomfyRts::ShadeCommandDown));
+ } else if (action.actionTypeId() == venetianBlindPercentageActionTypeId) {
+ requestMap.insert("target", action.paramValue(venetianBlindPercentageActionPercentageParamTypeId).toUInt());
+ } else if (action.actionTypeId() == venetianBlindAngleActionTypeId) {
+ url = somfy->tiltCommandUrl();
+ State angleState = thing->state(venetianBlindAngleStateTypeId);
+ int minValue = angleState.minValue().toInt();
+ int maxValue = angleState.maxValue().toInt();
+ int angle = action.paramValue(venetianBlindAngleActionAngleParamTypeId).toInt();
+ int percentage = calculatePercentageFromAngle(minValue, maxValue, angle);
+ qCDebug(dcESPSomfyRTS()) << "######" << percentage;
+ requestMap.insert("target", percentage);
+ }
+
+ QNetworkRequest request(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ qCDebug(dcESPSomfyRTS()) << "PUT" << url.toString() << qUtf8Printable(QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Compact));
+ QNetworkReply *reply = hardwareManager()->networkManager()->put(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Compact));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, info, [reply, info](){
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qCWarning(dcESPSomfyRTS()) << "Could not execute command on" << info->thing() << "because the network request finished with error" << reply->errorString();
+ info->finish(Thing::ThingErrorHardwareFailure);
+ return;
+ }
+
+ qCDebug(dcESPSomfyRTS()) << "Executed command successfully on" << info->thing();
+ info->finish(Thing::ThingErrorNoError);
+ });
+ }
+}
+
+int IntegrationPluginEspSomfyRts::calculateAngleFromPercentage(int minAngle, int maxAngle, int percentage)
+{
+ int minValue = qMin(minAngle, maxAngle);
+ int maxValue = qMax(minAngle, maxAngle);
+ int range = maxValue - minValue;
+ int angle = std::round(range * percentage / 100.0) + minAngle;
+ //qCDebug(dcESPSomfyRTS()) << "Calculate angle" << angle << "for percentage" << percentage << "min:" << minValue << "max:" << maxValue << "range:" << range;
+ return angle;
+}
+
+int IntegrationPluginEspSomfyRts::calculatePercentageFromAngle(int minAngle, int maxAngle, int angle)
+{
+ int minValue = qMin(minAngle, maxAngle);
+ int maxValue = qMax(minAngle, maxAngle);
+ int range = maxValue - minValue;
+ int percentage = std::round(angle * 100.0 / range) + 50;
+ //qCDebug(dcESPSomfyRTS()) << "Calculated percentage" << percentage << "for angle" << angle << "min:" << minValue << "max:" << maxValue << "range:" << range;
+ // FIXME: check the percentage of the negative part if asymetric
+ return percentage;
+}
+
+void IntegrationPluginEspSomfyRts::createThingForShade(const QVariantMap &shadeMap, const ThingId &parentThingId)
+{
+ QString shadeName = shadeMap.value("name").toString();
+ uint shadeId = shadeMap.value("shadeId").toUInt();
+ EspSomfyRts::ShadeType shadeType = static_cast(shadeMap.value("shadeType").toInt());
+
+ qCDebug(dcESPSomfyRTS()) << "Creating thing for" << shadeType << shadeId << shadeName;
+
+ ThingDescriptor desciptor;
+ ThingDescriptors desciptors;
+
+ switch (shadeType) {
+ case EspSomfyRts::ShadeTypeAwning:
+ desciptor = ThingDescriptor(awningThingClassId, shadeName);
+ desciptor.setParams(ParamList() << Param(awningThingShadeIdParamTypeId, shadeId));
+ desciptor.setParentId(parentThingId);
+ desciptors.append(desciptor);
+ break;
+ case EspSomfyRts::ShadeTypeBlind:
+ desciptor = ThingDescriptor(venetianBlindThingClassId, shadeName);
+ desciptor.setParams(ParamList() << Param(venetianBlindThingShadeIdParamTypeId, shadeId));
+ desciptor.setParentId(parentThingId);
+ desciptors.append(desciptor);
+ break;
+ default:
+ break;
+ }
+
+ if (desciptors.isEmpty())
+ return;
+
+ emit autoThingsAppeared(desciptors);
+}
+
+void IntegrationPluginEspSomfyRts::processShadeState(Thing *thing, const QVariantMap &shadeState)
+{
+ if (thing->thingClassId() == awningThingClassId) {
+
+ if (shadeState.contains("position"))
+ thing->setStateValue(awningPercentageStateTypeId, shadeState.value("position").toInt());
+
+ if (shadeState.contains("direction"))
+ thing->setStateValue(awningMovingStateTypeId, shadeState.value("direction").toInt() != EspSomfyRts::MovingDirectionRest);
+ return;
+ }
+
+ if (thing->thingClassId() == venetianBlindThingClassId) {
+ if (shadeState.contains("position"))
+ thing->setStateValue(venetianBlindPercentageStateTypeId, shadeState.value("position").toInt());
+
+ if (shadeState.contains("direction"))
+ thing->setStateValue(venetianBlindMovingStateTypeId, shadeState.value("direction").toInt() != EspSomfyRts::MovingDirectionRest);
+
+ State angleState = thing->state(venetianBlindAngleStateTypeId);
+ int angle = calculateAngleFromPercentage(angleState.minValue().toInt(), angleState.maxValue().toInt(), shadeState.value("tiltPosition").toInt());
+ thing->setStateValue(venetianBlindAngleStateTypeId, angle);
+ return;
+ }
+}
+
+void IntegrationPluginEspSomfyRts::synchronizeShades(Thing *thing)
+{
+ EspSomfyRts *somfy = m_somfys.value(thing);
+ qCDebug(dcESPSomfyRTS()) << "Synchronize shades of" << thing->name() << somfy->address().toString();
+
+ QUrl url = somfy->shadesUrl();
+ QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, thing, [this, reply, thing](){
+
+ if (reply->error() != QNetworkReply::NoError) {
+ qCDebug(dcESPSomfyRTS()) << "Get shades reply finished with error" << reply->errorString();
+ return;
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError);
+ if (jsonError.error != QJsonParseError::NoError) {
+ qCWarning(dcESPSomfyRTS()) << "Get shades reply contains invalid JSON data" << jsonError.errorString();
+ return;
+ }
+
+ QList handledThingIds;
+ QVariantList shadesList = jsonDoc.toVariant().toList();
+
+ // Get shades we need to add
+ QList shadesToCreateThingFor;
+ foreach (const QVariant &shadeVariant, shadesList) {
+ QVariantMap shadeMap = shadeVariant.toMap();
+
+ // Check if we have a thing for this shade ID
+ uint shadeId = shadeMap.value("shadeId").toUInt();
+ if (!m_shadeThings.contains(shadeId)) {
+ shadesToCreateThingFor.append(shadeMap);
+ } else {
+ // We already have a shade for this map, let's update the states
+ processShadeState(m_shadeThings.value(shadeId), shadeMap);
+ handledThingIds.append(m_shadeThings.value(shadeId)->id());
+ }
+
+ // TODO: check if a shade has changed the type, in that case,
+ // remove the old one and recreate a new one with the matching thing class
+ }
+
+ // Remove things if shade does not exist any more
+ foreach (Thing *existingThing, myThings().filterByParentId(thing->id())) {
+ if (!handledThingIds.contains(existingThing->id())) {
+ qCDebug(dcESPSomfyRTS()) << "Removing thing" << existingThing << "because the shade with ID" << existingThing->paramValue("shadeId").toUInt() << "does not exist any more on the ESP Somfy RTS.";
+ emit autoThingDisappeared(existingThing->id());
+ }
+ }
+
+ // Add things for shades new shades
+ foreach (const QVariantMap &shadeMap, shadesToCreateThingFor) {
+ createThingForShade(shadeMap, thing->id());
+ }
+ });
+}
+
+void IntegrationPluginEspSomfyRts::onEspSomfyConnectedChanged(Thing *thing, bool connected)
+{
+ thing->setStateValue(espSomfyRtsConnectedStateTypeId, connected);
+ foreach(Thing *childThing, myThings().filterByParentId(thing->id())) {
+ childThing->setStateValue("connected", connected);
+ }
+
+ if (connected) {
+ synchronizeShades(thing);
+ }
+}
diff --git a/espsomfyrts/integrationpluginespsomfyrts.h b/espsomfyrts/integrationpluginespsomfyrts.h
new file mode 100644
index 00000000..51984553
--- /dev/null
+++ b/espsomfyrts/integrationpluginespsomfyrts.h
@@ -0,0 +1,83 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2024, 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 INTEGRATIONPLUGINESPSOMFYRTS_H
+#define INTEGRATIONPLUGINESPSOMFYRTS_H
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include "extern-plugininfo.h"
+
+#include "espsomfyrts.h"
+
+class IntegrationPluginEspSomfyRts: public IntegrationPlugin
+{
+ Q_OBJECT
+
+ Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginespsomfyrts.json")
+ Q_INTERFACES(IntegrationPlugin)
+
+public:
+ explicit IntegrationPluginEspSomfyRts();
+
+ void init() override;
+ void discoverThings(ThingDiscoveryInfo *info) override;
+ void setupThing(ThingSetupInfo *info) override;
+ void postSetupThing(Thing *thing) override;
+ void thingRemoved(Thing *thing) override;
+
+ void executeAction(ThingActionInfo *info) override;
+
+private:
+ PluginTimer *m_refreshTimer = nullptr;
+ QHash m_somfys;
+ QHash m_shadeThings;
+
+ int calculateAngleFromPercentage(int minAngle, int maxAngle, int percentage);
+ int calculatePercentageFromAngle(int minAngle, int maxAngle, int angle);
+
+ void createThingForShade(const QVariantMap &shadeMap, const ThingId &parentThingId);
+ void processShadeState(Thing *thing, const QVariantMap &shadeState);
+ void synchronizeShades(Thing *thing);
+
+ ThingClassId getThingClassForShadeType(const QVariantMap &shadeMap);
+
+private slots:
+ void onEspSomfyConnectedChanged(Thing *thing, bool connected);
+
+};
+
+#endif // INTEGRATIONPLUGINESPSOMFYRTS_H
+
diff --git a/espsomfyrts/integrationpluginespsomfyrts.json b/espsomfyrts/integrationpluginespsomfyrts.json
new file mode 100644
index 00000000..d96be5f3
--- /dev/null
+++ b/espsomfyrts/integrationpluginespsomfyrts.json
@@ -0,0 +1,222 @@
+{
+ "name": "ESPSomfyRTS",
+ "displayName": "ESPSomfy-RTS",
+ "id": "6937979e-1ee9-499e-9116-8e8dc25d87b6",
+ "vendors": [
+ {
+ "name": "ESPSomfyRTS",
+ "displayName": "ESPSomfy-RTS",
+ "id": "ed38d638-7402-4afb-b4c9-71324e1d7a04",
+ "thingClasses": [
+ {
+ "name": "espSomfyRts",
+ "displayName": "ESPSomfy-RTS",
+ "id": "9a477bbe-81f0-46ad-ae62-715c2bba2f1f",
+ "createMethods": ["Discovery", "User"],
+ "interfaces": ["gateway", "wirelessconnectable" ],
+ "paramTypes": [
+ {
+ "id": "0e30e30f-ad96-417e-b739-cac85f75de39",
+ "name":"macAddress",
+ "displayName": "MAC address",
+ "type": "QString"
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "84e20ff2-2f48-44e6-b8f4-f9708cf2f187",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "5fece91a-6166-4e62-9510-ed97d48bec15",
+ "name": "signalStrength",
+ "displayName": "Signal strength",
+ "displayNameEvent": "Signal strength changed",
+ "type": "uint",
+ "unit": "Percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "defaultValue": 0,
+ "cached": false
+ },
+ {
+ "id": "1d919783-00a2-42f3-87a4-54a69040db4f",
+ "name": "firmwareVersion",
+ "displayName": "Firmware version",
+ "type": "QString",
+ "defaultValue": "",
+ "cached": true
+ }
+ ]
+ },
+ {
+ "name": "awning",
+ "displayName": "Awning",
+ "id": "1e76805f-ecba-45b3-ae84-bab3be60420e",
+ "createMethods": ["Auto"],
+ "interfaces": [ "extendedawning", "connectable" ],
+ "paramTypes": [
+ {
+ "id": "2b69a4ca-61d4-4436-9e95-dcf5a7b88e72",
+ "name":"shadeId",
+ "displayName": "ID",
+ "type": "uint"
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "81548389-ab52-4bee-b539-fab59dbc95a8",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "f1eaff9d-2e91-4f60-a10b-448bf0b2cd2a",
+ "name": "name",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "d2fb13d5-b575-46ef-a1c8-1d211fb14673",
+ "name": "moving",
+ "type": "bool",
+ "defaultValue": false,
+ "displayName": "Moving",
+ "displayNameEvent": "Moving changed"
+ },
+ {
+ "id": "4baecbcd-0407-4892-b679-45460a643322",
+ "name": "percentage",
+ "displayName": "Percentage",
+ "type": "int",
+ "unit": "Percentage",
+ "displayNameEvent": "Percentage changed",
+ "writable": true,
+ "displayNameAction": "Set percentage",
+ "defaultValue": 0,
+ "minValue": 0,
+ "maxValue": 100
+ }
+ ],
+ "actionTypes": [
+ {
+ "id": "8173003e-ed97-44d1-84b4-c37d34b8916b",
+ "name": "open",
+ "displayName": "Open"
+ },
+ {
+ "id": "9cbfea42-28f0-4359-8bf0-5e2f22238bb8",
+ "name": "stop",
+ "displayName": "My"
+ },
+ {
+ "id": "04189d1e-61b4-413b-8bfd-e6fb522f8b4a",
+ "name": "close",
+ "displayName": "Close"
+ }
+ ]
+ },
+ {
+ "name": "venetianBlind",
+ "displayName": "",
+ "id": "a8d077c9-b73c-47a3-a3ae-161c785a60c6",
+ "createMethods": ["Auto"],
+ "interfaces": [ "venetianblind", "connectable" ],
+ "paramTypes": [
+ {
+ "id": "7e728ef3-03ce-4671-93ce-fdcd51a496f8",
+ "name":"shadeId",
+ "displayName": "ID",
+ "type": "uint"
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "ade34009-bb6c-41fc-86dc-fc59c9cbca2f",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "8b7b37ed-d494-4004-870f-59836b007c45",
+ "name": "name",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "b4039247-eef3-4e9e-b1e7-31ed6c94d253",
+ "name": "moving",
+ "type": "bool",
+ "defaultValue": false,
+ "displayName": "Moving",
+ "displayNameEvent": "Moving changed"
+ },
+ {
+ "id": "a6cd9038-a6dd-48dc-97a3-3940cc443221",
+ "name": "percentage",
+ "displayName": "Percentage",
+ "displayNameAction": "Set percentage",
+ "type": "int",
+ "unit": "Percentage",
+ "writable": true,
+ "defaultValue": 0,
+ "minValue": 0,
+ "maxValue": 100
+ },
+ {
+ "id": "047c47c3-4cc1-4ccb-a351-09dc5976e3d6",
+ "name": "angle",
+ "displayName": "Angle",
+ "displayNameAction": "Set angle",
+ "type": "int",
+ "unit": "Degree",
+ "writable": true,
+ "defaultValue": 0,
+ "minValue": -90,
+ "maxValue": 90
+ }
+ ],
+ "actionTypes": [
+ {
+ "id": "e7b8557b-4121-4007-b027-136af7c01a1d",
+ "name": "open",
+ "displayName": "Open"
+ },
+ {
+ "id": "a9675717-3df3-4fa4-8fae-409f845cbb08",
+ "name": "stop",
+ "displayName": "My"
+ },
+ {
+ "id": "0bf42e90-9ccd-4054-adb8-f29c6d1876e9",
+ "name": "close",
+ "displayName": "Close"
+ },
+ {
+ "id": "49c55b11-d61a-4fd8-94ef-7a06e1827f77",
+ "name": "stepUp",
+ "displayName": "Step up"
+ },
+ {
+ "id": "dead4739-e0ad-4cea-8c99-9d6f04f519fd",
+ "name": "stepDown",
+ "displayName": "Step down"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/espsomfyrts/meta.json b/espsomfyrts/meta.json
new file mode 100644
index 00000000..77573dfc
--- /dev/null
+++ b/espsomfyrts/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "ESPSOmfy-RTS",
+ "tagline": "Connect to ESP Somfy RTS device and control your shades",
+ "icon": "espsomfy-rts.png",
+ "stability": "community",
+ "offline": true,
+ "technologies": [
+ "network"
+ ],
+ "categories": [
+ "appliance"
+ ]
+}
diff --git a/espsomfyrts/translations/6937979e-1ee9-499e-9116-8e8dc25d87b6-en_US.ts b/espsomfyrts/translations/6937979e-1ee9-499e-9116-8e8dc25d87b6-en_US.ts
new file mode 100644
index 00000000..c2ac0f6d
--- /dev/null
+++ b/espsomfyrts/translations/6937979e-1ee9-499e-9116-8e8dc25d87b6-en_US.ts
@@ -0,0 +1,165 @@
+
+
+
+
+ ESPSomfyRTS
+
+
+
+ Angle
+ The name of the ParamType (ThingClass: venetianBlind, ActionType: angle, ID: {047c47c3-4cc1-4ccb-a351-09dc5976e3d6})
+----------
+The name of the StateType ({047c47c3-4cc1-4ccb-a351-09dc5976e3d6}) of ThingClass venetianBlind
+
+
+
+
+ Awning
+ The name of the ThingClass ({1e76805f-ecba-45b3-ae84-bab3be60420e})
+
+
+
+
+
+ Close
+ The name of the ActionType ({0bf42e90-9ccd-4054-adb8-f29c6d1876e9}) of ThingClass venetianBlind
+----------
+The name of the ActionType ({04189d1e-61b4-413b-8bfd-e6fb522f8b4a}) of ThingClass awning
+
+
+
+
+
+
+
+
+ Connected
+ The name of the StateType ({8b7b37ed-d494-4004-870f-59836b007c45}) of ThingClass venetianBlind
+----------
+The name of the StateType ({ade34009-bb6c-41fc-86dc-fc59c9cbca2f}) of ThingClass venetianBlind
+----------
+The name of the StateType ({f1eaff9d-2e91-4f60-a10b-448bf0b2cd2a}) of ThingClass awning
+----------
+The name of the StateType ({81548389-ab52-4bee-b539-fab59dbc95a8}) of ThingClass awning
+----------
+The name of the StateType ({84e20ff2-2f48-44e6-b8f4-f9708cf2f187}) of ThingClass espSomfyRts
+
+
+
+
+
+
+ ESPSomfy-RTS
+ The name of the ThingClass ({9a477bbe-81f0-46ad-ae62-715c2bba2f1f})
+----------
+The name of the vendor ({ed38d638-7402-4afb-b4c9-71324e1d7a04})
+----------
+The name of the plugin ESPSomfyRTS ({6937979e-1ee9-499e-9116-8e8dc25d87b6})
+
+
+
+
+ Firmware version
+ The name of the StateType ({1d919783-00a2-42f3-87a4-54a69040db4f}) of ThingClass espSomfyRts
+
+
+
+
+
+ ID
+ The name of the ParamType (ThingClass: venetianBlind, Type: thing, ID: {7e728ef3-03ce-4671-93ce-fdcd51a496f8})
+----------
+The name of the ParamType (ThingClass: awning, Type: thing, ID: {2b69a4ca-61d4-4436-9e95-dcf5a7b88e72})
+
+
+
+
+ MAC address
+ The name of the ParamType (ThingClass: espSomfyRts, Type: thing, ID: {0e30e30f-ad96-417e-b739-cac85f75de39})
+
+
+
+
+
+ Moving
+ The name of the StateType ({b4039247-eef3-4e9e-b1e7-31ed6c94d253}) of ThingClass venetianBlind
+----------
+The name of the StateType ({d2fb13d5-b575-46ef-a1c8-1d211fb14673}) of ThingClass awning
+
+
+
+
+
+ My
+ The name of the ActionType ({a9675717-3df3-4fa4-8fae-409f845cbb08}) of ThingClass venetianBlind
+----------
+The name of the ActionType ({9cbfea42-28f0-4359-8bf0-5e2f22238bb8}) of ThingClass awning
+
+
+
+
+
+ Open
+ The name of the ActionType ({e7b8557b-4121-4007-b027-136af7c01a1d}) of ThingClass venetianBlind
+----------
+The name of the ActionType ({8173003e-ed97-44d1-84b4-c37d34b8916b}) of ThingClass awning
+
+
+
+
+
+
+
+ Percentage
+ The name of the ParamType (ThingClass: venetianBlind, ActionType: percentage, ID: {a6cd9038-a6dd-48dc-97a3-3940cc443221})
+----------
+The name of the StateType ({a6cd9038-a6dd-48dc-97a3-3940cc443221}) of ThingClass venetianBlind
+----------
+The name of the ParamType (ThingClass: awning, ActionType: percentage, ID: {4baecbcd-0407-4892-b679-45460a643322})
+----------
+The name of the StateType ({4baecbcd-0407-4892-b679-45460a643322}) of ThingClass awning
+
+
+
+
+ Set angle
+ The name of the ActionType ({047c47c3-4cc1-4ccb-a351-09dc5976e3d6}) of ThingClass venetianBlind
+
+
+
+
+
+ Set percentage
+ The name of the ActionType ({a6cd9038-a6dd-48dc-97a3-3940cc443221}) of ThingClass venetianBlind
+----------
+The name of the ActionType ({4baecbcd-0407-4892-b679-45460a643322}) of ThingClass awning
+
+
+
+
+ Signal strength
+ The name of the StateType ({5fece91a-6166-4e62-9510-ed97d48bec15}) of ThingClass espSomfyRts
+
+
+
+
+ Step down
+ The name of the ActionType ({dead4739-e0ad-4cea-8c99-9d6f04f519fd}) of ThingClass venetianBlind
+
+
+
+
+ Step up
+ The name of the ActionType ({49c55b11-d61a-4fd8-94ef-7a06e1827f77}) of ThingClass venetianBlind
+
+
+
+
+ IntegrationPluginEspSomfyRts
+
+
+ Unable to discover devices in your network.
+
+
+
+
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index b03b0950..32eae9f8 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -20,6 +20,7 @@ PLUGIN_DIRS = \
dynatrace \
easee \
elgato \
+ espsomfyrts \
eq-3 \
espuino \
evbox \