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 \