From 61f7364f8ad6cdc2657ddbc40065ed2be8b61e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 19 Sep 2024 12:59:48 +0200 Subject: [PATCH] New plugin: ESP Somfy RTS --- debian/control | 9 + debian/nymea-plugin-espsomfyrts.install.in | 2 + espsomfyrts/README.md | 7 + espsomfyrts/espsomfy-rts.png | Bin 0 -> 28984 bytes espsomfyrts/espsomfyrts.cpp | 249 ++++++++++ espsomfyrts/espsomfyrts.h | 135 ++++++ espsomfyrts/espsomfyrts.pro | 13 + espsomfyrts/espsomfyrtsdiscovery.cpp | 118 +++++ espsomfyrts/espsomfyrtsdiscovery.h | 73 +++ espsomfyrts/integrationpluginespsomfyrts.cpp | 438 ++++++++++++++++++ espsomfyrts/integrationpluginespsomfyrts.h | 83 ++++ espsomfyrts/integrationpluginespsomfyrts.json | 222 +++++++++ espsomfyrts/meta.json | 13 + ...7979e-1ee9-499e-9116-8e8dc25d87b6-en_US.ts | 165 +++++++ nymea-plugins.pro | 1 + 15 files changed, 1528 insertions(+) create mode 100644 debian/nymea-plugin-espsomfyrts.install.in create mode 100644 espsomfyrts/README.md create mode 100644 espsomfyrts/espsomfy-rts.png create mode 100644 espsomfyrts/espsomfyrts.cpp create mode 100644 espsomfyrts/espsomfyrts.h create mode 100644 espsomfyrts/espsomfyrts.pro create mode 100644 espsomfyrts/espsomfyrtsdiscovery.cpp create mode 100644 espsomfyrts/espsomfyrtsdiscovery.h create mode 100644 espsomfyrts/integrationpluginespsomfyrts.cpp create mode 100644 espsomfyrts/integrationpluginespsomfyrts.h create mode 100644 espsomfyrts/integrationpluginespsomfyrts.json create mode 100644 espsomfyrts/meta.json create mode 100644 espsomfyrts/translations/6937979e-1ee9-499e-9116-8e8dc25d87b6-en_US.ts 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 0000000000000000000000000000000000000000..d16fc347c3dd0eeea977933aa0a4a2ab844dea02 GIT binary patch literal 28984 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 literal 0 HcmV?d00001 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 \