Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions de_web_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,31 @@ DeRestPluginPrivate *DeRestPluginPrivate::instance()
return plugin;
}

void DeRestPluginPrivate::apsdeDataIndicationWebSocket(const deCONZ::ApsDataIndication &ind, Device *device)
{
// if (ind.clusterId() == 0x0019)
// return;

if (device)
{
QByteArray data;
{
QDataStream stream(&data, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::LittleEndian);
ind.writeToStream(stream);
}

QVariantMap map;
map[QLatin1String("t")] = QLatin1String("event");
map[QLatin1String("e")] = QLatin1String("aps.ind");
map[QLatin1String("r")] = QLatin1String("devices");
map[QLatin1String("uniqueid")] = device->item(RAttrUniqueId)->toLatin1String();
map["ind"] = data.toHex();

webSocketServer->broadcastTextMessage(Json::serialize(map));
}
}

void DeRestPluginPrivate::apsdeDataIndicationDevice(const deCONZ::ApsDataIndication &ind, Device *device)
{
if (!device)
Expand Down Expand Up @@ -1247,6 +1272,7 @@ void DeRestPluginPrivate::apsdeDataIndication(const deCONZ::ApsDataIndication &i
auto *device = DEV_GetDevice(m_devices, ind.srcAddress().ext());

apsdeDataIndicationDevice(ind, device);
apsdeDataIndicationWebSocket(ind, device);

if ((ind.profileId() == HA_PROFILE_ID) || (ind.profileId() == ZLL_PROFILE_ID))
{
Expand Down
1 change: 1 addition & 0 deletions de_web_plugin_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,7 @@ public Q_SLOTS:
Resource *getResource(const char *resource, const QString &id = QString());
void announceUpnp();
void upnpReadyRead();
void apsdeDataIndicationWebSocket(const deCONZ::ApsDataIndication &ind, Device *device);
void apsdeDataIndicationDevice(const deCONZ::ApsDataIndication &ind, Device *device);
void apsdeDataIndication(const deCONZ::ApsDataIndication &ind);
void apsdeDataConfirm(const deCONZ::ApsDataConfirm &conf);
Expand Down
146 changes: 146 additions & 0 deletions rest_devices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ int RestDevices::handleApi(const ApiRequest &req, ApiResponse &rsp)
{
return putDeviceInstallCode(req, rsp);
}
// PUT /api/<apikey>/devices/<uniqueid>/aps
else if (req.hdr.pathComponentsCount() == 5 && req.hdr.httpMethod() == HttpPut && req.hdr.pathAt(4) == QLatin1String("aps"))
{
return putDeviceApsRequest(req, rsp);
}

return REQ_NOT_HANDLED;
}
Expand Down Expand Up @@ -1199,6 +1204,147 @@ int RestDevices::putDeviceInstallCode(const ApiRequest &req, ApiResponse &rsp)
return REQ_READY_SEND;
}

/*! PUT /api/<apikey>/devices/<uniqueid>/aps
\return REQ_READY_SEND
REQ_NOT_HANDLED

Sends an APS request to the device. Note this is for debug purpose only!
Don't use this for regular user facing clients. Bug reports on this API will be ignored.

{
"dst_ep": 0..255,
"profile_id": "hex string"
"cluster_id": "hex string"
"payload": "hex string",
}

*/
int RestDevices::putDeviceApsRequest(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 5);

bool ok;
U_SStream ss;
const QString &uniqueid = req.path[3];

const auto deviceKey = extAddressFromUniqueId(req.hdr.pathAt(3));

Device *device = DEV_GetDevice(plugin->m_devices, deviceKey);

rsp.httpStatus = device ? HttpStatusOk : HttpStatusNotFound;

if (!device)
{
return REQ_READY_SEND;
}

QVariant var = Json::parse(req.content, ok);
QVariantMap map = var.toMap();

if (!ok || map.isEmpty())
{
rsp.list.append(errorToMap(ERR_INVALID_JSON, QString("/devices/%1/aps").arg(uniqueid), QString("body contains invalid JSON")));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

if (!map.contains("dst_ep") || !map.contains("cluster_id") || !map.contains("profile_id") || !map.contains("payload"))
{
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/devices/%1/aps").arg(uniqueid), QString("missing parameters in body")));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

int dstEndpoint = map.value("dst_ep").toString().toInt(&ok, 0);
if (!ok || dstEndpoint < 0 || dstEndpoint > 255)
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/devices"), QString("invalid value, %1, for parameter, dst_ep").arg(map.value("dst_ep").toString())));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

int clusterId = map.value("cluster_id").toString().toInt(&ok, 0);
if (!ok || clusterId < 0 || clusterId > 0xFFFF)
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/devices"), QString("invalid value, %1, for parameter, cluster_id").arg(map.value("cluster_id").toString())));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

int profileId = map.value("profile_id").toString().toInt(&ok, 0);
if (!ok || profileId < 0 || profileId > 0xFFFF)
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/devices"), QString("invalid value, %1, for parameter, profile_id").arg(map.value("profile_id").toString())));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

QString payloadStr = map.value("payload").toString();
QByteArray payload;

if (payloadStr.size() >= 2)
{
ok = true;
for (int i = 0; i < payloadStr.length(); i++)
{
QChar ch = payloadStr[i];
if (ch.isDigit()) {}
else if (ch >= 'a' && ch <= 'f') {}
else if (ch >= 'A' && ch <= 'F') {}
else
{
ok = false;
break;
}

payload.append(ch.toLatin1());
}
}

if (!ok || (payload.size() & 1)) // must be even number of hex digits
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/devices"), QString("invalid value, %1, for parameter, payload").arg(payloadStr)));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

payload = QByteArray::fromHex(payload);

if (payload.size())
{
deCONZ::Address dstAddr;
dstAddr.setExt(deviceKey);

deCONZ::ApsDataRequest req;
req.setTxOptions(deCONZ::ApsTxAcknowledgedTransmission);
req.setAsdu(payload);
req.setProfileId((uint16_t)profileId);
req.setClusterId((uint16_t)clusterId);
req.setDstEndpoint((uint8_t)dstEndpoint);
req.setDstAddressMode(deCONZ::ApsExtAddress);
req.setSrcEndpoint(plugin->endpoint());
req.dstAddress() = dstAddr;

QVariantMap m;

int ret = deCONZ::ApsController::instance()->apsdeDataRequest(req);
if (ret == deCONZ::Success)
{
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState["aps.id"] = (int)req.id();
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
}

rsp.list.append(errorToMap(ERR_INTERNAL_ERROR, QString("/devices"), "internal error, sending APS request"));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}

int RestDevices::putDeviceReloadDDF(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 6);
Expand Down
1 change: 1 addition & 0 deletions rest_devices.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public Q_SLOTS:
int putDeviceInstallCode(const ApiRequest &req, ApiResponse &rsp);
int putDeviceReloadDDF(const ApiRequest &req, ApiResponse &rsp);
int putDeviceSetDDFPolicy(const ApiRequest &req, ApiResponse &rsp);
int putDeviceApsRequest(const ApiRequest &req, ApiResponse &rsp);

DeRestPluginPrivate *plugin = nullptr;
RestDevicesPrivate *d = nullptr;
Expand Down
91 changes: 81 additions & 10 deletions websocket_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@
#include "deconz/dbg_trace.h"
#include "deconz/util.h"
#include "websocket_server.h"
#include "json.h"

class WebSocketServerPrivate
{
public:
QWebSocketServer *srv;
std::vector<QWebSocket*> clients;
};

/*! Constructor.
*/
WebSocketServer::WebSocketServer(QObject *parent, uint16_t wsPort) :
QObject(parent)
QObject(parent),
d_ptr(new WebSocketServerPrivate)
{
srv = new QWebSocketServer("deconz", QWebSocketServer::NonSecureMode, this);
d_ptr->srv = new QWebSocketServer("deconz", QWebSocketServer::NonSecureMode, this);
QWebSocketServer *srv = d_ptr->srv;

QHostAddress address;
QString addrArg = deCONZ::appArgumentString("--http-listen", QString());
Expand Down Expand Up @@ -60,9 +70,9 @@ WebSocketServer::WebSocketServer(QObject *parent, uint16_t wsPort) :
void WebSocketServer::handleExternalTcpSocket(const QHttpRequestHeader &hdr, QTcpSocket *sock)
{
U_ASSERT(sock);
if (srv)
if (d_ptr && d_ptr->srv)
{
srv->handleConnection(sock);
d_ptr->srv->handleConnection(sock);
}
else
{
Expand All @@ -75,28 +85,38 @@ void WebSocketServer::handleExternalTcpSocket(const QHttpRequestHeader &hdr, QTc
*/
quint16 WebSocketServer::port() const
{
return srv && srv->isListening() ? srv->serverPort() : 0;
if (!d_ptr || !d_ptr->srv)
return 0;

return d_ptr->srv->isListening() ? d_ptr->srv->serverPort() : 0;
}

/*! Handler for new client connections.
*/
void WebSocketServer::onNewConnection()
{
while (srv->hasPendingConnections())
if (!d_ptr || !d_ptr->srv)
return;

while (d_ptr->srv->hasPendingConnections())
{
QWebSocket *sock = srv->nextPendingConnection();
QWebSocket *sock = d_ptr->srv->nextPendingConnection();
DBG_Printf(DBG_INFO, "New websocket %s:%u\n", qPrintable(sock->peerAddress().toString()), sock->peerPort());
connect(sock, &QWebSocket::disconnected, this, &WebSocketServer::onSocketDisconnected);
connect(sock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));
connect(sock, &QWebSocket::textMessageReceived, this, &WebSocketServer::onTextMessageReceived);
clients.push_back(sock);
d_ptr->clients.push_back(sock);
}
}

/*! Handle websocket disconnected signal.
*/
void WebSocketServer::onSocketDisconnected()
{
if (!d_ptr)
return;

auto &clients = d_ptr->clients;
for (size_t i = 0; i < clients.size(); i++)
{
QWebSocket *sock = qobject_cast<QWebSocket*>(sender());
Expand All @@ -116,6 +136,10 @@ void WebSocketServer::onSocketDisconnected()
void WebSocketServer::onSocketError(QAbstractSocket::SocketError err)
{
Q_UNUSED(err);
if (!d_ptr)
return;

auto &clients = d_ptr->clients;
for (size_t i = 0; i < clients.size(); i++)
{
QWebSocket *sock = qobject_cast<QWebSocket*>(sender());
Expand All @@ -131,20 +155,63 @@ void WebSocketServer::onSocketError(QAbstractSocket::SocketError err)

void WebSocketServer::onTextMessageReceived(const QString &message)
{
Q_UNUSED(message);
// DBG_Printf(DBG_INFO, "Websocket received: %s\n", qPrintable(message));

QWebSocket *sock = qobject_cast<QWebSocket*>(sender());
if (!sock)
return;

auto map = Json::parse(message).toMap();

if (map.isEmpty())
return;

// {"enable_aps":true}
if (map.contains("enable_aps"))
{
auto val = map["enable_aps"];
if (val.type() == QVariant::Bool)
{
sock->setProperty("enable_aps", val);
}
}

}

/*! Broadcasts a APS indication message to all connected clients which have 'enable_aps' set to true.
\param msg the message as JSON string
*/
void WebSocketServer::broadcastApsIndication(const QString &msg)
{
if (!d_ptr)
return;

auto &clients = d_ptr->clients;
for (size_t i = 0; i < clients.size(); i++)
{
QWebSocket *sock = clients[i];
if (sock->property("enable_aps").toBool())
{
sock->sendTextMessage(msg);
sock->flush();
}
}
}

/*! Broadcasts a message to all connected clients.
\param msg the message as JSON string
*/
void WebSocketServer::broadcastTextMessage(const QString &msg)
{
if (!d_ptr)
return;

auto &clients = d_ptr->clients;
for (size_t i = 0; i < clients.size(); i++)
{
QWebSocket *sock = clients[i];
qint64 ret = sock->sendTextMessage(msg);
DBG_Printf(DBG_INFO_L2, "Websocket %s:%u send message: %s (ret = %d)\n", qPrintable(sock->peerAddress().toString()), sock->peerPort(), qPrintable(msg), (int)ret);
// DBG_Printf(DBG_INFO_L2, "Websocket %s:%u send message: %s (ret = %d)\n", qPrintable(sock->peerAddress().toString()), sock->peerPort(), qPrintable(msg), (int)ret);
sock->flush();
}
}
Expand All @@ -153,6 +220,10 @@ void WebSocketServer::broadcastTextMessage(const QString &msg)
*/
void WebSocketServer::flush()
{
if (!d_ptr)
return;

auto &clients = d_ptr->clients;
for (size_t i = 0; i < clients.size(); i++)
{
QWebSocket *sock = clients[i];
Expand Down
Loading