From 3c1e6314ad4cd165bd9a3d02eba1c9f7e62fb881 Mon Sep 17 00:00:00 2001 From: Manuel Pietschmann Date: Sun, 18 Sep 2016 14:55:44 +0200 Subject: [PATCH] Functional version of remote gateway control and group cascades in same IP network --- database.cpp | 187 +++++++++++++++++++++++++++++ de_web_plugin.cpp | 8 ++ de_web_plugin_private.h | 5 + gateway.cpp | 258 ++++++++++++++++++++++++++++++++++++---- gateway.h | 19 +++ gateway_scanner.cpp | 31 +++-- rest_gateways.cpp | 232 +++++++++++++++++++++++++++++++++--- 7 files changed, 690 insertions(+), 50 deletions(-) diff --git a/database.cpp b/database.cpp index e796911e78..85ee625cae 100644 --- a/database.cpp +++ b/database.cpp @@ -14,6 +14,7 @@ #include "de_web_plugin.h" #include "de_web_plugin_private.h" #include "deconz/dbg_trace.h" +#include "gateway.h" #include "json.h" /****************************************************************************** @@ -32,6 +33,7 @@ static int sqliteLoadAllRulesCallback(void *user, int ncols, char **colval , cha static int sqliteLoadAllSensorsCallback(void *user, int ncols, char **colval , char **colname); static int sqliteGetAllLightIdsCallback(void *user, int ncols, char **colval , char **colname); static int sqliteGetAllSensorIdsCallback(void *user, int ncols, char **colval , char **colname); +static int sqliteLoadAllGatewaysCallback(void *user, int ncols, char **colval , char **colname); /****************************************************************************** Implementation @@ -72,6 +74,7 @@ void DeRestPluginPrivate::initDb() "CREATE TABLE IF NOT EXISTS sensors (sid TEXT PRIMARY KEY, name TEXT, type TEXT, modelid TEXT, manufacturername TEXT, uniqueid TEXT, swversion TEXT, state TEXT, config TEXT, fingerprint TEXT, deletedState TEXT, mode TEXT)", "CREATE TABLE IF NOT EXISTS scenes (gsid TEXT PRIMARY KEY, gid TEXT, sid TEXT, name TEXT, transitiontime TEXT, lights TEXT)", "CREATE TABLE IF NOT EXISTS schedules (id TEXT PRIMARY KEY, json TEXT)", + "CREATE TABLE IF NOT EXISTS gateways (uuid TEXT PRIMARY KEY, name TEXT, ip TEXT, port TEXT, pairing TEXT, apikey TEXT, cgroups TEXT)", "ALTER TABLE sensors add column fingerprint TEXT", "ALTER TABLE sensors add column deletedState TEXT", "ALTER TABLE sensors add column mode TEXT", @@ -189,6 +192,7 @@ void DeRestPluginPrivate::readDb() loadAllRulesFromDb(); loadAllSchedulesFromDb(); loadAllSensorsFromDb(); + loadAllGatewaysFromDb(); } /*! Sqlite callback to load authentification data. @@ -1684,6 +1688,35 @@ void DeRestPluginPrivate::loadAllSensorsFromDb() } } +/*! Loads all gateways from database + */ +void DeRestPluginPrivate::loadAllGatewaysFromDb() +{ + int rc; + char *errmsg = 0; + + DBG_Assert(db != 0); + + if (!db) + { + return; + } + + QString sql(QLatin1String("SELECT * FROM gateways")); + + DBG_Printf(DBG_INFO_L2, "sql exec %s\n", qPrintable(sql)); + rc = sqlite3_exec(db, qPrintable(sql), sqliteLoadAllGatewaysCallback, this, &errmsg); + + if (rc != SQLITE_OK) + { + if (errmsg) + { + DBG_Printf(DBG_ERROR_L2, "sqlite3_exec %s, error: %s\n", qPrintable(sql), errmsg); + sqlite3_free(errmsg); + } + } +} + /*! Sqlite callback to load all light ids into temporary array. */ static int sqliteGetAllLightIdsCallback(void *user, int ncols, char **colval , char **colname) @@ -1806,6 +1839,82 @@ static int sqliteGetAllSensorIdsCallback(void *user, int ncols, char **colval , return 0; } +static int sqliteLoadAllGatewaysCallback(void *user, int ncols, char **colval , char **colname) +{ + DBG_Assert(user != 0); + + if (!user || (ncols <= 0)) + { + return 0; + } + + DeRestPluginPrivate *d = static_cast(user); + + int idxUuid = -1; + int idxName = -1; + int idxIp = -1; + int idxPort = -1; + int idxApikey = -1; + int idxPairing = -1; + int idxCgroups = -1; + + for (int i = 0; i < ncols; i++) + { + if (colval[i] && (colval[i][0] != '\0')) + { + if (strcmp(colname[i], "uuid") == 0) { idxUuid = i; } + else if (strcmp(colname[i], "name") == 0) { idxName = i; } + else if (strcmp(colname[i], "ip") == 0) { idxIp = i; } + else if (strcmp(colname[i], "port") == 0) { idxPort = i; } + else if (strcmp(colname[i], "apikey") == 0) { idxApikey = i; } + else if (strcmp(colname[i], "pairing") == 0) { idxPairing = i; } + else if (strcmp(colname[i], "cgroups") == 0) { idxCgroups = i; } + } + } + + if (idxUuid == -1) + { + return 0; // required + } + + Gateway *gw = new Gateway(d); + + gw->setUuid(colval[idxUuid]); + if (idxName != -1) { gw->setName(colval[idxName]); } + if (idxIp != -1) { gw->setAddress(QHostAddress(colval[idxIp])); } + if (idxPort != -1) { gw->setPort(QString(colval[idxPort]).toUShort()); } + if (idxApikey != -1) { gw->setApiKey(colval[idxApikey]); } + if (idxPairing != -1) { gw->setPairingEnabled(colval[idxPairing][0] == '1'); } + if (idxCgroups != -1 && colval[idxCgroups][0] == '[') // must be json array + { + bool ok; + QVariant var = Json::parse(colval[idxCgroups], ok); + + if (ok && var.type() == QVariant::List) + { + QVariantList ls = var.toList(); + for (int i = 0; i < ls.size(); i++) + { + QVariantMap e = ls[i].toMap(); + if (e.contains(QLatin1String("lg")) && e.contains(QLatin1String("rg"))) + { + double lg = e[QLatin1String("lg")].toDouble(); + double rg = e[QLatin1String("rg")].toDouble(); + + if (lg > 0 && lg <= 0xfffful && rg > 0 && rg <= 0xfffful) + { + gw->addCascadeGroup(lg, rg); + } + } + } + } + } + gw->setNeedSaveDatabase(false); + d->gateways.push_back(gw); + + return 0; +} + /*! Determines a unused id for a sensor. */ int DeRestPluginPrivate::getFreeSensorId() @@ -2062,6 +2171,84 @@ void DeRestPluginPrivate::saveDb() saveDatabaseItems &= ~DB_USERPARAM; } + // save gateways + if (saveDatabaseItems & DB_GATEWAYS) + { + std::vector::iterator i = gateways.begin(); + std::vector::iterator end = gateways.end(); + + for (; i != end; ++i) + { + Gateway *gw = *i; + if (!gw->needSaveDatabase()) + { + continue; + } + + gw->setNeedSaveDatabase(false); + + if (!gw->pairingEnabled()) + { + // delete gateways from db (if exist) + QString sql = QString(QLatin1String("DELETE FROM gateways WHERE uuid='%1'")).arg(gw->uuid()); + + DBG_Printf(DBG_INFO_L2, "sql exec %s\n", qPrintable(sql)); + errmsg = NULL; + rc = sqlite3_exec(db, sql.toUtf8().constData(), NULL, NULL, &errmsg); + + if (rc != SQLITE_OK) + { + if (errmsg) + { + DBG_Printf(DBG_ERROR, "sqlite3_exec failed: %s, error: %s\n", qPrintable(sql), errmsg); + sqlite3_free(errmsg); + } + } + } + else + { + QByteArray cgroups("[]"); + if (!gw->cascadeGroups().empty()) + { + QVariantList ls; + for (size_t i = 0; i < gw->cascadeGroups().size(); i++) + { + const Gateway::CascadeGroup &cg = gw->cascadeGroups()[i]; + QVariantMap e; + e[QLatin1String("lg")] = (double)cg.local; + e[QLatin1String("rg")] = (double)cg.remote; + ls.push_back(e); + } + cgroups = Json::serialize(ls); + } + + QString sql = QString(QLatin1String("REPLACE INTO gateways (uuid, name, ip, port, pairing, apikey, cgroups) VALUES ('%1', '%2', '%3', '%4', '%5', '%6', '%7')")) + .arg(gw->uuid()) + .arg(gw->name()) + .arg(gw->address().toString()) + .arg(gw->port()) + .arg((gw->pairingEnabled() ? '1' : '0')) + .arg(gw->apiKey()) + .arg(qPrintable(cgroups)); + + DBG_Printf(DBG_INFO_L2, "sql exec %s\n", qPrintable(sql)); + errmsg = NULL; + rc = sqlite3_exec(db, sql.toUtf8().constData(), NULL, NULL, &errmsg); + + if (rc != SQLITE_OK) + { + if (errmsg) + { + DBG_Printf(DBG_ERROR, "sqlite3_exec failed: %s, error: %s\n", qPrintable(sql), errmsg); + sqlite3_free(errmsg); + } + } + } + } + + saveDatabaseItems &= ~DB_GATEWAYS; + } + // save nodes if (saveDatabaseItems & DB_LIGHTS) { diff --git a/de_web_plugin.cpp b/de_web_plugin.cpp index 13966bbd9b..ad7ec05517 100644 --- a/de_web_plugin.cpp +++ b/de_web_plugin.cpp @@ -326,16 +326,24 @@ void DeRestPluginPrivate::apsdeDataIndication(const deCONZ::ApsDataIndication &i case SCENE_CLUSTER_ID: handleSceneClusterIndication(task, ind, zclFrame); + handleClusterIndicationGateways(ind, zclFrame); break; case OTAU_CLUSTER_ID: otauDataIndication(ind, zclFrame); break; + case COMMISSIONING_CLUSTER_ID: handleCommissioningClusterIndication(task, ind, zclFrame); break; + + case LEVEL_CLUSTER_ID: + handleClusterIndicationGateways(ind, zclFrame); + break; + case ONOFF_CLUSTER_ID: handleOnOffClusterIndication(task, ind, zclFrame); + handleClusterIndicationGateways(ind, zclFrame); break; default: diff --git a/de_web_plugin_private.h b/de_web_plugin_private.h index 239cf9bb56..396172e630 100644 --- a/de_web_plugin_private.h +++ b/de_web_plugin_private.h @@ -208,6 +208,7 @@ #define DB_RULES 0x00000040 #define DB_SENSORS 0x00000080 #define DB_USERPARAM 0x00000100 +#define DB_GATEWAYS 0x00000200 #define DB_LONG_SAVE_DELAY (15 * 60 * 1000) // 15 minutes #define DB_SHORT_SAVE_DELAY (5 * 1 * 1000) // 5 seconds @@ -504,6 +505,8 @@ class DeRestPluginPrivate : public QObject int getAllGateways(const ApiRequest &req, ApiResponse &rsp); int getGatewayState(const ApiRequest &req, ApiResponse &rsp); int setGatewayState(const ApiRequest &req, ApiResponse &rsp); + int addCascadeGroup(const ApiRequest &req, ApiResponse &rsp); + int deleteCascadeGroup(const ApiRequest &req, ApiResponse &rsp); void gatewayToMap(const ApiRequest &req, const Gateway *gw, QVariantMap &map); // REST API configuration @@ -832,6 +835,7 @@ public Q_SLOTS: void handleGroupClusterIndication(TaskItem &task, const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame); void handleSceneClusterIndication(TaskItem &task, const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame); void handleOnOffClusterIndication(TaskItem &task, const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame); + void handleClusterIndicationGateways(const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame); void handleCommissioningClusterIndication(TaskItem &task, const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame); bool handleMgmtBindRspConfirm(const deCONZ::ApsDataConfirm &conf); void handleDeviceAnnceIndication(const deCONZ::ApsDataIndication &ind); @@ -868,6 +872,7 @@ public Q_SLOTS: void loadSceneFromDb(Scene *scene); void loadAllRulesFromDb(); void loadAllSensorsFromDb(); + void loadAllGatewaysFromDb(); int getFreeLightId(); int getFreeSensorId(); void saveDb(); diff --git a/gateway.cpp b/gateway.cpp index 7ae9784867..c109d26247 100644 --- a/gateway.cpp +++ b/gateway.cpp @@ -7,11 +7,32 @@ #include "gateway.h" #include "json.h" + +#define ONOFF_COMMAND_OFF 0x00 +#define ONOFF_COMMAND_ON 0x01 +#define ONOFF_COMMAND_TOGGLE 0x02 +#define ONOFF_COMMAND_ON_WITH_TIMED_OFF 0x42 + enum GW_Event { ActionProcess, EventTimeout, - EventResponse + EventResponse, + EventCommandAdded +}; + +class Command +{ +public: + quint16 groupId; + quint16 clusterId; + quint8 commandId; + + union { + quint8 sceneId; + quint8 level; + } param; + quint16 transitionTime; }; class GatewayPrivate @@ -27,6 +48,7 @@ class GatewayPrivate Gateway::State state; bool pairingEnabled; + bool needSaveDatabase; QString apikey; QString name; QString uuid; @@ -37,7 +59,10 @@ class GatewayPrivate QNetworkAccessManager *manager; QBuffer *reqBuffer; QNetworkReply *reply; + int pings; std::vector groups; + std::vector cascadeGroups; + std::vector commands; }; Gateway::Gateway(QObject *parent) : @@ -45,8 +70,10 @@ Gateway::Gateway(QObject *parent) : d_ptr(new GatewayPrivate) { Q_D(Gateway); + d->pings = 0; d->state = Gateway::StateOffline; d->pairingEnabled = false; + d->needSaveDatabase = false; d->reply = 0; d->manager = new QNetworkAccessManager(this); connect(d->manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(finished(QNetworkReply*))); @@ -75,6 +102,7 @@ void Gateway::setAddress(const QHostAddress &address) if (d->address != address) { d->address = address; + d->needSaveDatabase = true; } } @@ -90,6 +118,7 @@ void Gateway::setName(const QString &name) if (d->name != name) { d->name = name; + d->needSaveDatabase = true; } } @@ -105,6 +134,7 @@ void Gateway::setUuid(const QString &uuid) if (d->uuid != uuid) { d->uuid = uuid; + d->needSaveDatabase = true; } } @@ -126,6 +156,7 @@ void Gateway::setPort(quint16 port) if (d->port != port) { d->port = port; + d->needSaveDatabase = true; } } @@ -135,9 +166,16 @@ void Gateway::setApiKey(const QString &apiKey) if (d->apikey != apiKey) { d->apikey = apiKey; + d->needSaveDatabase = true; } } +const QString &Gateway::apiKey() const +{ + Q_D(const Gateway); + return d->apikey; +} + bool Gateway::pairingEnabled() const { Q_D(const Gateway); @@ -150,6 +188,7 @@ void Gateway::setPairingEnabled(bool pairingEnabled) if (d->pairingEnabled != pairingEnabled) { d->pairingEnabled = pairingEnabled; + d->needSaveDatabase = true; } } @@ -159,12 +198,117 @@ Gateway::State Gateway::state() const return d->state; } +bool Gateway::needSaveDatabase() const +{ + Q_D(const Gateway); + return d->needSaveDatabase; +} + +void Gateway::setNeedSaveDatabase(bool save) +{ + Q_D(Gateway); + d->needSaveDatabase = save; +} + +void Gateway::addCascadeGroup(quint16 local, quint16 remote) +{ + Q_D(Gateway); + for (size_t i = 0; i < d->cascadeGroups.size(); i++) + { + if (d->cascadeGroups[i].local == local && d->cascadeGroups[i].remote == remote) + { + // already known + return; + } + } + + CascadeGroup cg; + cg.local = local; + cg.remote = remote; + d->cascadeGroups.push_back(cg); + d->needSaveDatabase = true; +} + +void Gateway::removeCascadeGroup(quint16 local, quint16 remote) +{ + Q_D(Gateway); + for (size_t i = 0; i < d->cascadeGroups.size(); i++) + { + if (d->cascadeGroups[i].local == local && d->cascadeGroups[i].remote == remote) + { + d->cascadeGroups[i].local = d->cascadeGroups.back().local; + d->cascadeGroups[i].remote = d->cascadeGroups.back().remote; + d->cascadeGroups.pop_back(); + d->needSaveDatabase = true; + return; + } + } +} + +void Gateway::handleGroupCommand(const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame) +{ + Q_D(Gateway); + if (d->state != StateConnected) + { + return; + } + + if (ind.dstAddressMode() != deCONZ::ApsGroupAddress) + { + return; + } + + for (size_t j = 0; j < d->cascadeGroups.size(); j++) + { + const CascadeGroup &cg = d->cascadeGroups[j]; + if (cg.local == ind.dstAddress().group()) + { + Command cmd; + + // filter + if (ind.clusterId() == 0x0005 && zclFrame.commandId() == 0x05) // recall scene + { + if (zclFrame.payload().size() < 3) // sanity + continue; + + // payload U16 group, U8 scene + cmd.param.sceneId = zclFrame.payload().at(2); + } + else if (ind.clusterId() == 0x0006) // onoff + { + } +// else if (ind.clusterId() == 0x0008) // level +// { +// } + else + { + continue; + } + + cmd.clusterId = ind.clusterId(); + cmd.groupId = cg.remote; + cmd.commandId = zclFrame.commandId(); + cmd.transitionTime = 0; + d->commands.push_back(cmd); + d->handleEvent(EventCommandAdded); + + DBG_Printf(DBG_INFO, "GW %s forward command 0x%02X on cluster 0x%04X on group 0x%04X to remote group 0x%04X\n", qPrintable(d->name), zclFrame.commandId(), ind.clusterId(), cg.local, cg.remote); + } + } +} + const std::vector &Gateway::groups() const { Q_D(const Gateway); return d->groups; } +const std::vector &Gateway::cascadeGroups() const +{ + Q_D(const Gateway); + return d->cascadeGroups; +} + void Gateway::timerFired() { Q_D(Gateway); @@ -218,6 +362,8 @@ void GatewayPrivate::handleEventStateOffline(GW_Event event) return; } + pings = 0; + QString url; url.sprintf("http://%s:%u/api/%s/config", qPrintable(address.toString()), port, qPrintable(apikey)); @@ -242,13 +388,13 @@ void GatewayPrivate::handleEventStateOffline(GW_Event event) if (code == 403) { state = Gateway::StateNotAuthorized; - startTimer(1000, ActionProcess); + startTimer(5000, ActionProcess); } else if (code == 200) { checkConfigResponse(r->readAll()); state = Gateway::StateConnected; - startTimer(500, ActionProcess); + startTimer(5000, ActionProcess); } else { @@ -283,6 +429,8 @@ void GatewayPrivate::handleEventStateNotAuthorized(GW_Event event) return; } + pings = 0; + // try to create user account QString url; url.sprintf("http://%s:%u/api/", qPrintable(address.toString()), port); @@ -307,7 +455,6 @@ void GatewayPrivate::handleEventStateNotAuthorized(GW_Event event) } else if (event == EventResponse) { - QNetworkReply *r = reply; if (reply) { @@ -315,17 +462,16 @@ void GatewayPrivate::handleEventStateNotAuthorized(GW_Event event) reply = 0; int code = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - { - QByteArray data = r->readAll(); - DBG_Printf(DBG_INFO, "GW create user reply %d: %s\n", code, qPrintable(data)); - } +// { +// QByteArray data = r->readAll(); +// DBG_Printf(DBG_INFO, "GW create user reply %d: %s\n", code, qPrintable(data)); +// } r->deleteLater(); if (code == 403) { - // retry - startTimer(10000, ActionProcess); + // gateway must be unlocked ... } else if (code == 200) { @@ -333,6 +479,12 @@ void GatewayPrivate::handleEventStateNotAuthorized(GW_Event event) state = Gateway::StateOffline; startTimer(100, ActionProcess); } + + // retry + if (!timer->isActive()) + { + startTimer(10000, ActionProcess); + } } } else if (event == EventTimeout) @@ -346,14 +498,66 @@ void GatewayPrivate::handleEventStateConnected(GW_Event event) { if (event == ActionProcess) { - QString url; - url.sprintf("http://%s:%u/api/%s/groups", - qPrintable(address.toString()), port, qPrintable(apikey)); + Q_ASSERT(reply == 0); - reply = manager->get(QNetworkRequest(url)); - QObject::connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), - manager->parent(), SLOT(error(QNetworkReply::NetworkError))); + if (commands.empty()) + { + QString url; + url.sprintf("http://%s:%u/api/%s/groups", + qPrintable(address.toString()), port, qPrintable(apikey)); + + pings++; + reply = manager->get(QNetworkRequest(url)); + QObject::connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + manager->parent(), SLOT(error(QNetworkReply::NetworkError))); + } + else + { + QString url; + QVariantMap map; + const Command &cmd = commands.back(); + + if (cmd.clusterId == 0x0005 && cmd.commandId == 0x05) // recall scene + { + url.sprintf("http://%s:%u/api/%s/groups/%u/scenes/%u/recall", + qPrintable(address.toString()), port, qPrintable(apikey), cmd.groupId, cmd.param.sceneId); + } + else if (cmd.clusterId == 0x0006) + { + url.sprintf("http://%s:%u/api/%s/groups/%u/action", + qPrintable(address.toString()), port, qPrintable(apikey), cmd.groupId); + + if (cmd.commandId == ONOFF_COMMAND_ON) { map[QLatin1String("on")] = true; } + if (cmd.commandId == ONOFF_COMMAND_OFF) { map[QLatin1String("on")] = false; } + } + + commands.pop_back(); + + if (url.isEmpty()) + { + startTimer(50, EventTimeout); + return; + } + + QString json; + if (!map.isEmpty()) + { + json = deCONZ::jsonStringFromMap(map); + } + else + { + json = QLatin1String("{}"); + } + reqBuffer->close(); + reqBuffer->setData(json.toUtf8()); + reqBuffer->open(QBuffer::ReadOnly); + QNetworkRequest req(url); + reply = manager->put(req, reqBuffer); + + QObject::connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), + manager->parent(), SLOT(error(QNetworkReply::NetworkError))); + } startTimer(1000, EventTimeout); } @@ -366,12 +570,14 @@ void GatewayPrivate::handleEventStateConnected(GW_Event event) reply = 0; int code = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (code == 200) { - //state = Gateway::StateConnected; // ok check again later - checkGroupsResponse(r->readAll()); + if (r->url().toString().endsWith(QLatin1String("/groups"))) + { + pings = 0; + checkGroupsResponse(r->readAll()); + } startTimer(15000, ActionProcess); } else @@ -396,10 +602,20 @@ void GatewayPrivate::handleEventStateConnected(GW_Event event) } r->deleteLater(); } - DBG_Printf(DBG_INFO, "request timeout in connected state switch to offline state\n"); - state = Gateway::StateOffline; + if (pings > 5) + { + DBG_Printf(DBG_INFO, "max request timeout in connected state switch to offline state\n"); + state = Gateway::StateOffline; + } startTimer(5000, ActionProcess); } + else if (event == EventCommandAdded) + { + if (!reply) // not busy + { + startTimer(50, ActionProcess); + } + } } void GatewayPrivate::checkConfigResponse(const QByteArray &data) diff --git a/gateway.h b/gateway.h index 8f0bef9a30..c82e6db824 100644 --- a/gateway.h +++ b/gateway.h @@ -5,6 +5,11 @@ #include #include +namespace deCONZ { + class ApsDataIndication; + class ZclFrame; +} + class GatewayPrivate; class Gateway : public QObject @@ -18,6 +23,13 @@ class Gateway : public QObject QString name; }; + class CascadeGroup + { + public: + quint16 local; + quint16 remote; + }; + enum State { StateOffline, @@ -37,11 +49,18 @@ class Gateway : public QObject quint16 port() const; void setPort(quint16 port); void setApiKey(const QString &apiKey); + const QString &apiKey() const; bool pairingEnabled() const; void setPairingEnabled(bool pairingEnabled); State state() const; + bool needSaveDatabase() const; + void setNeedSaveDatabase(bool save); + void addCascadeGroup(quint16 local, quint16 remote); + void removeCascadeGroup(quint16 local, quint16 remote); + void handleGroupCommand(const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame); const std::vector &groups() const; + const std::vector &cascadeGroups() const; signals: diff --git a/gateway_scanner.cpp b/gateway_scanner.cpp index d7535e6dee..5f1a039660 100644 --- a/gateway_scanner.cpp +++ b/gateway_scanner.cpp @@ -222,6 +222,7 @@ void GatewayScannerPrivate::handleEvent(ScanEvent event) QNetworkRequest req = r->request(); DBG_Printf(DBG_INFO, "reply code %d from %s\n", code, qPrintable(req.url().toString())); + QString name; bool isGateway = false; char buf[256]; qint64 n = 0; @@ -239,21 +240,26 @@ void GatewayScannerPrivate::handleEvent(ScanEvent event) isGateway = true; } } - else + else if (strstr(buf, "")) { - const char *uuid = strstr(buf, "uuid:"); - const char *end = uuid ? strstr(uuid, "") : 0; - if (uuid && end) - { - uuid += strlen("uuid:"); - buf[end - buf] = '\0'; - QString name; - q->foundGateway(scanIp, scanPort, uuid, name); - break; - } + const char *start = strchr(buf, '>') + 1; + const char *end = strstr(start, ""); + if (!end || start == end) + continue; + buf[end - buf] = '\0'; + name = start; + } + else if (strstr(buf, "uuid:")) + { + const char *start = strchr(buf, ':') + 1; + const char *end = strstr(start, ""); + if (!end || start == end) + continue; + buf[end - buf] = '\0'; + q->foundGateway(scanIp, scanPort, start, name); + break; } } - } r->deleteLater(); } @@ -298,7 +304,6 @@ void GatewayScannerPrivate::queryNextIp() return; } - scanIp = interfaces.back(); scanPort = 80; diff --git a/rest_gateways.cpp b/rest_gateways.cpp index 7b930a6017..19cf5c82de 100644 --- a/rest_gateways.cpp +++ b/rest_gateways.cpp @@ -53,6 +53,16 @@ int DeRestPluginPrivate::handleGatewaysApi(const ApiRequest &req, ApiResponse &r { return setGatewayState(req, rsp); } + // POST /api//gateways//cascadegroup + else if ((req.path.size() == 5) && (req.hdr.method() == QLatin1String("POST")) && (req.path[4] == QLatin1String("cascadegroup"))) + { + return addCascadeGroup(req, rsp); + } + // DELETE /api//gateways//cascadegroup + else if ((req.path.size() == 5) && (req.hdr.method() == QLatin1String("DELETE")) && (req.path[4] == QLatin1String("cascadegroup"))) + { + return deleteCascadeGroup(req, rsp); + } return REQ_NOT_HANDLED; } @@ -72,6 +82,12 @@ int DeRestPluginPrivate::getAllGateways(const ApiRequest &req, ApiResponse &rsp) } } + // user is on the gateway page, run scanner in background + if (!gwScanner->isRunning()) + { + gwScanner->startScan(); + } + if (rsp.map.isEmpty()) { rsp.str = "{}"; @@ -87,13 +103,6 @@ int DeRestPluginPrivate::getAllGateways(const ApiRequest &req, ApiResponse &rsp) */ int DeRestPluginPrivate::getGatewayState(const ApiRequest &req, ApiResponse &rsp) { - DBG_Assert(req.path.size() == 4); - - if (req.path.size() != 4) - { - return REQ_NOT_HANDLED; - } - rsp.httpStatus = HttpStatusOk; bool ok; @@ -118,19 +127,12 @@ int DeRestPluginPrivate::getGatewayState(const ApiRequest &req, ApiResponse &rsp return REQ_READY_SEND; } -/*! PUT /api//gateways/ +/*! PUT /api//gateways//state \return REQ_READY_SEND REQ_NOT_HANDLED */ int DeRestPluginPrivate::setGatewayState(const ApiRequest &req, ApiResponse &rsp) { - DBG_Assert(req.path.size() == 5); - - if (req.path.size() != 5) - { - return REQ_NOT_HANDLED; - } - rsp.httpStatus = HttpStatusOk; bool ok; @@ -179,6 +181,11 @@ int DeRestPluginPrivate::setGatewayState(const ApiRequest &req, ApiResponse &rsp } } + if (gw->needSaveDatabase()) + { + queSaveDb(DB_GATEWAYS, DB_SHORT_SAVE_DELAY); + } + if (!rsp.list.empty()) { return REQ_READY_SEND; @@ -187,6 +194,146 @@ int DeRestPluginPrivate::setGatewayState(const ApiRequest &req, ApiResponse &rsp return REQ_NOT_HANDLED; } +/*! POST /api//gateways//cascadegroup + \return REQ_READY_SEND + REQ_NOT_HANDLED + */ +int DeRestPluginPrivate::addCascadeGroup(const ApiRequest &req, ApiResponse &rsp) +{ + bool ok; + const QString &id = req.path[3]; + size_t idx = id.toUInt(&ok); + + if (!ok || idx == 0 || (idx - 1) >= gateways.size()) + { + rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, QString("/gateways/%1").arg(id), QString("resource, /gateways/%1, not available").arg(id))); + rsp.httpStatus = HttpStatusNotFound; + return REQ_READY_SEND; + } + + QVariant var = Json::parse(req.content, ok); + QVariantMap map = var.toMap(); + + if (!ok || map.isEmpty()) + { + rsp.httpStatus = HttpStatusBadRequest; + rsp.list.append(errorToMap(ERR_INVALID_JSON, QString("/gateways/%1/cascadegroup").arg(id), QLatin1String("body contains invalid JSON"))); + return REQ_READY_SEND; + } + + if (!map.contains(QLatin1String("local")) || !map.contains(QLatin1String("remote"))) + { + rsp.httpStatus = HttpStatusBadRequest; + rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/gateways/%1/casecadegroup").arg(id), "missing parameters in body")); + return REQ_READY_SEND; + } + + double lg = map[QLatin1String("local")].toDouble(&ok); + if (!ok || lg < 0 || lg > 0xffff) + { + rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/gateways/%1/casecadegroup/local").arg(id), QString("invalid value, %1, for parameter, local").arg(map[QLatin1String("local")].toString()))); + rsp.httpStatus = HttpStatusBadRequest; + return REQ_READY_SEND; + } + + double rg = map[QLatin1String("remote")].toDouble(&ok); + if (!ok || rg < 0 || rg > 0xffff) + { + rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/gateways/%1/casecadegroup/remote").arg(id), QString("invalid value, %1, for parameter, remote").arg(map[QLatin1String("remote")].toString()))); + rsp.httpStatus = HttpStatusBadRequest; + return REQ_READY_SEND; + } + + rsp.httpStatus = HttpStatusOk; + Gateway *gw = gateways[idx - 1]; + gw->addCascadeGroup(lg, rg); + + if (gw->needSaveDatabase()) + { + queSaveDb(DB_GATEWAYS, DB_SHORT_SAVE_DELAY); + } + + DBG_Printf(DBG_INFO, "Add cascade group %u||%u\n", (quint16)lg, (quint16)rg); + + QVariantMap rspItem; + QVariantMap rspItemState; + rspItemState[QString("/gateways/%1/casecadegroup").arg(id)] = map; + rspItem[QLatin1String("success")] = rspItemState; + rsp.list.append(rspItem); + + return REQ_READY_SEND; +} + +/*! DELETE /api//gateways//cascadegroup + \return REQ_READY_SEND + REQ_NOT_HANDLED + */ +int DeRestPluginPrivate::deleteCascadeGroup(const ApiRequest &req, ApiResponse &rsp) +{ + bool ok; + const QString &id = req.path[3]; + size_t idx = id.toUInt(&ok); + + if (!ok || idx == 0 || (idx - 1) >= gateways.size()) + { + rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, QString("/gateways/%1").arg(id), QString("resource, /gateways/%1, not available").arg(id))); + rsp.httpStatus = HttpStatusNotFound; + return REQ_READY_SEND; + } + + QVariant var = Json::parse(req.content, ok); + QVariantMap map = var.toMap(); + + if (!ok || map.isEmpty()) + { + rsp.httpStatus = HttpStatusBadRequest; + rsp.list.append(errorToMap(ERR_INVALID_JSON, QString("/gateways/%1/cascadegroup").arg(id), QLatin1String("body contains invalid JSON"))); + return REQ_READY_SEND; + } + + if (!map.contains(QLatin1String("local")) || !map.contains(QLatin1String("remote"))) + { + rsp.httpStatus = HttpStatusBadRequest; + rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/gateways/%1/casecadegroup").arg(id), "missing parameters in body")); + return REQ_READY_SEND; + } + + double lg = map[QLatin1String("local")].toDouble(&ok); + if (!ok || lg < 0 || lg > 0xffff) + { + rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/gateways/%1/casecadegroup/local").arg(id), QString("invalid value, %1, for parameter, local").arg(map[QLatin1String("local")].toString()))); + rsp.httpStatus = HttpStatusBadRequest; + return REQ_READY_SEND; + } + + double rg = map[QLatin1String("remote")].toDouble(&ok); + if (!ok || rg < 0 || rg > 0xffff) + { + rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/gateways/%1/casecadegroup/remote").arg(id), QString("invalid value, %1, for parameter, remote").arg(map[QLatin1String("remote")].toString()))); + rsp.httpStatus = HttpStatusBadRequest; + return REQ_READY_SEND; + } + + rsp.httpStatus = HttpStatusOk; + Gateway *gw = gateways[idx - 1]; + gw->removeCascadeGroup(lg, rg); + + if (gw->needSaveDatabase()) + { + queSaveDb(DB_GATEWAYS, DB_SHORT_SAVE_DELAY); + } + + DBG_Printf(DBG_INFO, "Remove cascade group %u||%u\n", (quint16)lg, (quint16)rg); + + QVariantMap rspItem; + QVariantMap rspItemState; + rspItemState[QString("/gateways/%1/casecadegroup").arg(id)] = map; + rspItem[QLatin1String("success")] = rspItemState; + rsp.list.append(rspItem); + + return REQ_READY_SEND; +} + /*! Puts all parameters in a map for later JSON serialization. */ void DeRestPluginPrivate::gatewayToMap(const ApiRequest &req, const Gateway *gw, QVariantMap &map) @@ -223,9 +370,36 @@ void DeRestPluginPrivate::gatewayToMap(const ApiRequest &req, const Gateway *gw, map[QLatin1String("groups")] = groups; } + if (!gw->cascadeGroups().empty()) + { + QVariantList cgs; + + for (size_t i = 0; i < gw->cascadeGroups().size(); i++) + { + const Gateway::CascadeGroup &g = gw->cascadeGroups()[i]; + QVariantMap cg; + cg[QLatin1String("local")] = QString::number(g.local); + cg[QLatin1String("remote")] = QString::number(g.remote); + cgs.push_back(cg); + } + + map[QLatin1String("cascadegroups")] = cgs; + } + switch (gw->state()) { - case Gateway::StateConnected: { map[QLatin1String("state")] = QLatin1String("connected"); } break; + case Gateway::StateConnected: + { + if (gw->pairingEnabled()) + { + map[QLatin1String("state")] = QLatin1String("connected"); + } + else + { + map[QLatin1String("state")] = QLatin1String("not authorized"); + } + } + break; case Gateway::StateNotAuthorized: { map[QLatin1String("state")] = QLatin1String("not authorized"); } break; case Gateway::StateOffline: { map[QLatin1String("state")] = QLatin1String("offline"); } break; default: { map[QLatin1String("state")] = QLatin1String("unknown"); } @@ -251,6 +425,17 @@ void DeRestPluginPrivate::foundGateway(quint32 ip, quint16 port, const QString & { gw->setAddress(QHostAddress(ip)); gw->setPort(port); + + } + + if (gw->name() != name && !name.isEmpty()) + { + gw->setName(name); + } + + if (gw->needSaveDatabase()) + { + queSaveDb(DB_GATEWAYS, DB_SHORT_SAVE_DELAY); } return; // already known @@ -269,3 +454,18 @@ void DeRestPluginPrivate::foundGateway(quint32 ip, quint16 port, const QString & DBG_Printf(DBG_INFO, "found gateway %s:%u\n", qPrintable(gw->address().toString()), port); gateways.push_back(gw); } + + +void DeRestPluginPrivate::handleClusterIndicationGateways(const deCONZ::ApsDataIndication &ind, deCONZ::ZclFrame &zclFrame) +{ + if (ind.dstAddressMode() != deCONZ::ApsGroupAddress) + { + return; + } + + for (size_t i = 0; i < gateways.size(); i++) + { + Gateway *gw = gateways[i]; + gw->handleGroupCommand(ind, zclFrame); + } +}