* Copyright (c) 2013-2025 dresden elektronik ingenieurtechnik gmbh.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
*/
#include <QString>
#include <QVariantMap>
#include <QProcess>
#include "de_web_plugin.h"
#include "de_web_plugin_private.h"
#include "product_match.h"
#include "database.h"
#include "device_descriptions.h"
#include "device_ddf_bundle.h"
#include "deconz/atom_table.h"
#include "deconz/u_assert.h"
#include "deconz/u_sstream_ex.h"
#include "deconz/u_memory.h"
#include "rest_devices.h"
#include "utils/scratchmem.h"
#include "json.h"
#include "crypto/mmohash.h"
#include "utils/ArduinoJson.h"
#include "utils/utils.h"
using JsonDoc = StaticJsonDocument<1024 * 1024 * 2>;
static void putJsonQVariantValue(JsonObject &obj, std::string key, const QVariant &value);
static void putJsonArrayQVariantValue(JsonArray &arr, const QVariant &value);
static RestDevicesPrivate *priv_;
class RestDevicesPrivate
{
public:
JsonDoc json;
char jsonBuffer[1024 * 1024];
};
RestDevices::RestDevices(QObject *parent) :
QObject(parent)
{
d = new RestDevicesPrivate;
priv_ = d;
plugin = qobject_cast<DeRestPluginPrivate*>(parent);
Q_ASSERT(plugin);
}
RestDevices::~RestDevices()
{
priv_ = nullptr;
delete d;
}
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int RestDevices::handleApi(const ApiRequest &req, ApiResponse &rsp)
{
if (req.hdr.pathComponentsCount() == 3 && req.hdr.httpMethod() == HttpGet)
{
return getAllDevices(req, rsp);
}
else if (req.hdr.pathComponentsCount() == 4 && req.hdr.httpMethod() == HttpGet)
{
return getDevice(req, rsp);
}
else if (req.path.size() == 6 && req.hdr.method() == QLatin1String("PUT") && req.path[4] == QLatin1String("ddf") && req.path[5] == QLatin1String("reload"))
{
return putDeviceReloadDDF(req, rsp);
}
else if (req.path.size() == 6 && req.hdr.method() == QLatin1String("PUT") && req.path[4] == QLatin1String("ddf") && req.path[5] == QLatin1String("policy"))
{
return putDeviceSetDDFPolicy(req, rsp);
}
else if (req.hdr.pathComponentsCount() == 5 && req.hdr.httpMethod() == HttpGet && req.hdr.pathAt(4) == QLatin1String("ddf"))
{
return getDeviceDDF(req, rsp);
}
else if (req.hdr.pathComponentsCount() == 5 && req.hdr.httpMethod() == HttpGet && req.hdr.pathAt(4) == QLatin1String("ddffull"))
{
return getDeviceDDF(req, rsp);
}
else if (req.hdr.pathComponentsCount() == 5 && req.hdr.httpMethod() == HttpGet && req.hdr.pathAt(4) == QLatin1String("introspect"))
{
return RIS_GetDeviceIntrospect(req, rsp);
}
else if (req.hdr.pathComponentsCount() > 5 && req.hdr.httpMethod() == HttpGet &&
req.hdr.pathAt(req.hdr.pathComponentsCount() - 1) == QLatin1String("introspect"))
{
return RIS_GetDeviceItemIntrospect(req, rsp);
}
else if (req.hdr.pathComponentsCount() == 5 && req.hdr.httpMethod() == HttpPut && req.hdr.pathAt(4) == QLatin1String("installcode"))
{
return putDeviceInstallCode(req, rsp);
}
return REQ_NOT_HANDLED;
}
static DeviceKey getDeviceKey(QLatin1String uniqueid)
{
DeviceKey result = 0;
const char *str = uniqueid.data();
if (uniqueid.size() < 23)
return result;
for (int pos = 0; pos < 23; pos++)
{
uint64_t ch = (unsigned)str[pos];
if (ch == ':' && (pos % 3) == 2)
continue;
result <<= 4;
if (ch >= '0' && ch <= '9') ch = ch - '0';
else if (ch >= 'a' && ch <= 'f') ch = (ch - 'a') + 10;
else if (ch >= 'A' && ch <= 'F') ch = (ch - 'A') + 10;
else
{
result = 0;
break;
}
result |= (ch & 0x0F);
}
return result;
}
and a ZDP reset will be send if possible.
*/
bool deleteSensor(Sensor *sensor, DeRestPluginPrivate *plugin)
{
if (sensor && plugin && sensor->deletedState() == Sensor::StateNormal)
{
sensor->setDeletedState(Sensor::StateDeleted);
sensor->setNeedSaveDatabase(true);
sensor->setResetRetryCount(10);
enqueueEvent(Event(sensor->prefix(), REventDeleted, sensor->id()));
return true;
}
return false;
}
and a ZDP reset will be send if possible.
*/
bool deleteLight(LightNode *lightNode, DeRestPluginPrivate *plugin)
{
if (lightNode && plugin && lightNode->state() == LightNode::StateNormal)
{
lightNode->setState(LightNode::StateDeleted);
lightNode->setResetRetryCount(10);
lightNode->setNeedSaveDatabase(true);
for (auto &group : lightNode->groups())
{
plugin->deleteLightFromScenes(lightNode->id(), group.id);
group.actions &= ~GroupInfo::ActionAddToGroup;
group.actions |= GroupInfo::ActionRemoveFromGroup;
if (group.state != GroupInfo::StateNotInGroup)
{
group.state = GroupInfo::StateNotInGroup;
}
}
enqueueEvent(Event(lightNode->prefix(), REventDeleted, lightNode->id()));
return true;
}
return false;
}
*/
bool RestDevices::deleteDevice(quint64 extAddr)
{
int count = 0;
for (auto &sensor : plugin->sensors)
{
if (sensor.address().ext() == extAddr && deleteSensor(&sensor, plugin))
{
count++;
}
}
for (auto &lightNode : plugin->nodes)
{
if (lightNode.address().ext() == extAddr && deleteLight(&lightNode, plugin))
{
count++;
}
}
if (count > 0)
{
plugin->queSaveDb(DB_SENSORS | DB_LIGHTS | DB_GROUPS | DB_SCENES, DB_SHORT_SAVE_DELAY);
}
plugin->deleteDeviceDb(generateUniqueId(extAddr, 0, 0));
enqueueEvent(Event(RDevices, REventDeleted, 0, extAddr));
return count > 0;
}
void RestDevices::handleEvent(const Event &event)
{
if (event.resource() == RDevices && event.what() == REventDeleted)
{
DEV_RemoveDevice(plugin->m_devices, event.deviceKey());
}
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int RestDevices::getAllDevices(const ApiRequest &req, ApiResponse &rsp)
{
Q_UNUSED(req)
rsp.httpStatus = HttpStatusOk;
for (const auto &d : plugin->m_devices)
{
Q_ASSERT(d);
rsp.list.push_back(d->item(RAttrUniqueId)->toString());
}
if (rsp.list.isEmpty())
{
rsp.str = QLatin1String("[]");
}
return REQ_READY_SEND;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
Unstable API to experiment: don't use in production!
*/
int RestDevices::getDevice(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 4);
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;
}
const DeviceDescription &ddf = plugin->deviceDescriptions->get(device);
if (ddf.isValid())
{
rsp.map["productid"] = ddf.product;
}
{
const ResourceItem *ddfPolicyItem = device->item(RAttrDdfPolicy);
if (ddfPolicyItem)
{
rsp.map["ddf_policy"] = ddfPolicyItem->toString();
}
}
if (ddf.storageLocation == deCONZ::DdfBundleLocation || ddf.storageLocation == deCONZ::DdfBundleUserLocation)
{
const ResourceItem *ddfHashItem = device->item(RAttrDdfHash);
if (ddfHashItem && ddfHashItem->toCString()[0] != '\0')
{
rsp.map["ddf_hash"] = ddfHashItem->toString();
}
}
QVariantList subDevices;
for (const auto &sub : device->subDevices())
{
QVariantMap map;
for (int i = 0; i < sub->itemCount(); i++)
{
auto *item = sub->itemForIndex(i);
Q_ASSERT(item);
if (item->descriptor().suffix == RStateLastUpdated ||
item->descriptor().suffix == RAttrId)
{
continue;
}
if (!item->isPublic())
{
continue;
}
const auto ls = QString(QLatin1String(item->descriptor().suffix)).split(QLatin1Char('/'));
if (ls.size() == 2)
{
if (item->descriptor().suffix == RAttrLastSeen || item->descriptor().suffix == RAttrLastAnnounced ||
item->descriptor().suffix == RAttrManufacturerName || item->descriptor().suffix == RAttrModelId ||
item->descriptor().suffix == RAttrSwVersion || item->descriptor().suffix == RAttrName)
{
if (!rsp.map.contains(ls.at(1)))
{
rsp.map[ls.at(1)] = item->toString();
}
}
else if (ls.at(0) == QLatin1String("attr"))
{
map[ls.at(1)] = item->toVariant();
}
else
{
QVariantMap m2;
if (map.contains(ls.at(0)))
{
m2 = map[ls.at(0)].toMap();
}
QVariantMap itemMap;
itemMap[QLatin1String("value")] = item->toVariant();
QDateTime dt = item->lastChanged().isValid() ? item->lastChanged().toUTC() : item->lastSet().toUTC();
dt.setOffsetFromUtc(0);
itemMap[QLatin1String("lastupdated")] = dt.toString(QLatin1String("yyyy-MM-ddTHH:mm:ssZ"));
m2[ls.at(1)] = itemMap;
map[ls.at(0)] = m2;
}
}
}
subDevices.push_back(map);
}
rsp.map["uniqueid"] = device->item(RAttrUniqueId)->toString();
rsp.map["subdevices"] = subDevices;
return REQ_READY_SEND;
}
static void putJsonArrayQVariantValue(JsonArray &arr, const QVariant &value)
{
if (value.type() == QVariant::String)
{
arr.add(value.toString().toStdString());
}
else if (value.type() == QVariant::Bool)
{
arr.add(value.toBool());
}
else if (value.type() == QVariant::Double)
{
arr.add(value.toDouble());
}
else if (value.type() == QVariant::Int)
{
arr.add(value.toInt());
}
else if (value.type() == QVariant::UInt)
{
arr.add(value.toUInt());
}
else if (value.type() == QVariant::ULongLong)
{
arr.add(uint64_t(value.toULongLong()));
}
else if (value.type() == QVariant::LongLong)
{
arr.add(int64_t(value.toLongLong()));
}
else if (value.type() == QVariant::List)
{
JsonArray arr1 = arr.createNestedArray();
const QVariantList ls = value.toList();
for (const auto &v : ls)
{
putJsonArrayQVariantValue(arr1, v);
}
}
else if (value.type() == QVariant::Map)
{
JsonObject obj1 = arr.createNestedObject();
const QVariantMap map = value.toMap();
auto i = map.constBegin();
const auto end = map.constEnd();
for (; i != end; ++i)
{
putJsonQVariantValue(obj1, i.key().toStdString(), i.value());
}
}
else
{
DBG_Printf(DBG_DDF, "DDF TODO %s:%d arr add type: %s\n", __FILE__, __LINE__, QVariant::typeToName(value.type()));
}
}
static void putJsonQVariantValue(JsonObject &obj, std::string key, const QVariant &value)
{
if (value.type() == QVariant::String)
{
obj[key] = value.toString().toStdString();
}
else if (value.type() == QVariant::Bool)
{
obj[key] = value.toBool();
}
else if (value.type() == QVariant::Double)
{
obj[key] = value.toDouble();
}
else if (value.type() == QVariant::Int)
{
obj[key] = value.toInt();
}
else if (value.type() == QVariant::UInt)
{
obj[key] = value.toUInt();
}
else if (value.type() == QVariant::ULongLong)
{
obj[key] = uint64_t(value.toULongLong());
}
else if (value.type() == QVariant::LongLong)
{
obj[key] = int64_t(value.toLongLong());
}
else if (value.type() == QVariant::List)
{
JsonArray arr = obj.createNestedArray(key);
const QVariantList ls = value.toList();
for (const auto &v : ls)
{
putJsonArrayQVariantValue(arr, v);
}
}
else if (value.type() == QVariant::Map)
{
JsonObject obj1 = obj.createNestedObject(key);
const QVariantMap map = value.toMap();
auto i = map.constBegin();
const auto end = map.constEnd();
for (; i != end; ++i)
{
putJsonQVariantValue(obj1, i.key().toStdString(), i.value());
}
}
else
{
DBG_Printf(DBG_DDF, "DDF TODO %s:%d obj.%s type: %s\n", __FILE__, __LINE__, key.c_str(), QVariant::typeToName(value.type()));
}
}
static void putItemParameter(JsonObject &item, const char *name, const QVariantMap ¶m)
{
JsonObject parse = item.createNestedObject(name);
const auto end = param.constEnd();
for (auto cur = param.constBegin(); cur != end; cur++)
{
if (cur.key() == QLatin1String("eval"))
{
if (!param.contains(QLatin1String("script")))
{
putJsonQVariantValue(parse, "eval", cur.value());
}
}
else
{
putJsonQVariantValue(parse, cur.key().toStdString(), cur.value());
}
}
}
bool ddfSerializeV1(JsonDoc &doc, const DeviceDescription &ddf, char *buf, size_t bufsize, bool ddfFull, bool prettyPrint)
{
doc.clear();
doc["schema"] = "devcap1.schema.json";
if (ddf.manufacturerNames.size() == 1)
{
doc["manufacturername"] = ddf.manufacturerNames.front().toStdString();
}
else
{
JsonArray arr = doc.createNestedArray("manufacturername");
for (const QString &i : ddf.manufacturerNames)
{
arr.add(i.toStdString());
}
}
if (ddf.modelIds.size() == 1)
{
doc["modelid"] = ddf.modelIds.front().toStdString();
}
else
{
JsonArray arr = doc.createNestedArray("modelid");
for (const QString &i : ddf.modelIds)
{
arr.add(i.toStdString());
}
}
if (!ddf.vendor.isEmpty())
{
doc["vendor"] = ddf.vendor.toStdString();
}
if (!ddf.product.isEmpty())
{
doc["product"] = ddf.product.toStdString();
}
if (ddf.sleeper >= 0)
{
doc["sleeper"] = ddf.sleeper > 0;
}
doc["status"] = ddf.status.toStdString();
if (!ddf.matchExpr.isEmpty())
{
doc["matchexpr"] = ddf.matchExpr.toStdString();
}
{
JsonArray subDevices = doc.createNestedArray("subdevices");
for (const DeviceDescription::SubDevice &sub : ddf.subDevices)
{
JsonObject subDevice = subDevices.createNestedObject();
subDevice["type"] = sub.type.toStdString();
subDevice["restapi"] = sub.restApi.toStdString();
JsonArray uuid = subDevice.createNestedArray("uuid");
for (const QString &i : sub.uniqueId)
{
uuid.add(i.toStdString());
}
if (!sub.meta.isEmpty())
{
putJsonQVariantValue(subDevice, "meta", sub.meta);
}
if (isValid(sub.fingerPrint))
{
char buf[16];
JsonObject fp = subDevice.createNestedObject("fingerprint");
snprintf(buf, sizeof(buf), "0x%04X", sub.fingerPrint.profileId);
fp["profile"] = std::string(buf);
snprintf(buf, sizeof(buf), "0x%04X", sub.fingerPrint.deviceId);
fp["device"] = std::string(buf);
snprintf(buf, sizeof(buf), "0x%02X", sub.fingerPrint.endpoint);
fp["endpoint"] = std::string(buf);
if (!sub.fingerPrint.inClusters.empty())
{
JsonArray inClusters = fp.createNestedArray("in");
for (const auto clusterId : sub.fingerPrint.inClusters)
{
snprintf(buf, sizeof(buf), "0x%04X", clusterId);
inClusters.add(std::string(buf));
}
}
if (!sub.fingerPrint.outClusters.empty())
{
JsonArray outClusters = fp.createNestedArray("out");
for (const auto clusterId : sub.fingerPrint.outClusters)
{
snprintf(buf, sizeof(buf), "0x%04X", clusterId);
outClusters.add(std::string(buf));
}
}
}
JsonArray items = subDevice.createNestedArray("items");
for (const DeviceDescription::Item &i : sub.items)
{
JsonObject item = items.createNestedObject();
if (i.isImplicit && !ddfFull)
{
item["name"] = i.name.c_str();
continue;
}
item["name"] = i.name.c_str();
if (!i.isPublic) { item["public"] = false; }
if (i.awake) { item["awake"] = true; }
if (!i.description.isEmpty())
{
item["description"] = i.description.toStdString();
}
if (i.refreshInterval > 0)
{
item["refresh.interval"] = i.refreshInterval;
}
if (!i.isStatic)
{
if (!i.readParameters.isNull() && (ddfFull || !i.isGenericRead)) { putItemParameter(item, "read", i.readParameters.toMap()); }
if (!i.writeParameters.isNull() && (ddfFull || !i.isGenericWrite)) { putItemParameter(item, "write", i.writeParameters.toMap()); }
if (!i.parseParameters.isNull() && (ddfFull || !i.isGenericParse)) { putItemParameter(item, "parse", i.parseParameters.toMap()); }
}
if (!i.defaultValue.isNull())
{
if (i.isStatic)
{
putJsonQVariantValue(item, "static", i.defaultValue);
}
else
{
putJsonQVariantValue(item, "default", i.defaultValue);
}
}
}
}
}
if (!ddf.bindings.empty())
{
JsonArray bindings = doc.createNestedArray("bindings");
for (const DDF_Binding &bnd : ddf.bindings)
{
JsonObject binding = bindings.createNestedObject();
if (bnd.isUnicastBinding) { binding["bind"] = "unicast"; }
else if (bnd.isGroupBinding)
{
binding["bind"] = "groupcast";
binding["config.group"] = bnd.configGroup;
}
binding["src.ep"] = bnd.srcEndpoint;
if (bnd.dstEndpoint > 0) { binding["dst.ep"] = bnd.dstEndpoint; }
char buf[16];
snprintf(buf, sizeof(buf), "0x%04X", bnd.clusterId);
binding["cl"] = std::string(buf);
if (!bnd.reporting.empty())
{
JsonArray reportings = binding.createNestedArray("report");
for (const DDF_ZclReport &rep: bnd.reporting)
{
JsonObject report = reportings.createNestedObject();
snprintf(buf, sizeof(buf), "0x%04X", rep.attributeId);
report["at"] = std::string(buf);
snprintf(buf, sizeof(buf), "0x%02X", rep.dataType);
report["dt"] = std::string(buf);
if (rep.manufacturerCode > 0)
{
snprintf(buf, sizeof(buf), "0x%04X", rep.manufacturerCode);
report["mf"] = std::string(buf);
}
report["min"] = rep.minInterval;
report["max"] = rep.maxInterval;
if (rep.reportableChange > 0)
{
snprintf(buf, sizeof(buf), "0x%08X", rep.reportableChange);
report["change"] = std::string(buf);
}
}
}
}
}
size_t sz = 0;
if (prettyPrint)
{
sz = serializeJsonPretty(doc, buf, bufsize);
}
else
{
sz = serializeJson(doc, buf, bufsize);
}
U_ASSERT(sz < bufsize);
DBG_Printf(DBG_INFO, "JSON serialized size %d\n", int(sz));
return sz > 0 && sz < bufsize;
}
QString DDF_ToJsonPretty(const DeviceDescription &ddf)
{
QString result;
if (priv_ && ddfSerializeV1(priv_->json, ddf, priv_->jsonBuffer, sizeof(priv_->jsonBuffer), false, true))
{
result = priv_->jsonBuffer;
}
return result;
}
int RestDevices::getDeviceDDF(const ApiRequest &req, ApiResponse &rsp)
{
const auto deviceKey = extAddressFromUniqueId(req.hdr.pathAt(3));
bool ddfFull = req.hdr.pathAt(4) == QLatin1String("ddffull");
Device *device = DEV_GetDevice(plugin->m_devices, deviceKey);
rsp.httpStatus = device ? HttpStatusOk : HttpStatusNotFound;
if (!device)
{
return REQ_READY_SEND;
}
DeviceDescription ddf = DeviceDescriptions::instance()->get(device);
if (ddf.isValid())
{
if (ddf.bindings.empty())
{
ddf.bindings = device->bindings();
}
if (ddfSerializeV1(d->json, ddf, d->jsonBuffer, sizeof(d->jsonBuffer), ddfFull, false))
{
rsp.str = d->jsonBuffer;
}
else
{
}
}
else
{
rsp.httpStatus = HttpStatusNotFound;
rsp.str = QLatin1String("{}");
}
return REQ_READY_SEND;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
Unstable API to experiment: don't use in production!
*/
int RIS_GetDeviceIntrospect(const ApiRequest &req, ApiResponse &rsp)
{
Q_UNUSED(req)
rsp.str = QLatin1String("{\"introspect\": false}");
return REQ_READY_SEND;
}
*/
QLatin1String RIS_DataTypeToString(ApiDataType type)
{
static const std::array<QLatin1String, 14> map = {
QLatin1String("unknown"),
QLatin1String("bool"),
QLatin1String("uint8"),
QLatin1String("uint16"),
QLatin1String("uint32"),
QLatin1String("uint64"),
QLatin1String("int8"),
QLatin1String("int16"),
QLatin1String("int32"),
QLatin1String("int64"),
QLatin1String("double"),
QLatin1String("string"),
QLatin1String("time"),
QLatin1String("timepattern")
};
if (type < map.size())
{
return map[type];
}
return map[0];
}
*/
QLatin1String RIS_ButtonEventActionToString(int buttonevent)
{
const uint action = buttonevent % 1000;
static std::array<QLatin1String, 11> map = {
QLatin1String("INITIAL_PRESS"),
QLatin1String("HOLD"),
QLatin1String("SHORT_RELEASE"),
QLatin1String("LONG_RELEASE"),
QLatin1String("DOUBLE_PRESS"),
QLatin1String("TREBLE_PRESS"),
QLatin1String("QUADRUPLE_PRESS"),
QLatin1String("SHAKE"),
QLatin1String("DROP"),
QLatin1String("TILT"),
QLatin1String("MANY_PRESS")
};
if (action < map.size())
{
return map[action];
}
return QLatin1String("UNKNOWN");
}
*/
QVariantMap RIS_IntrospectGenericItem(const ResourceItemDescriptor &rid)
{
QVariantMap result;
result[QLatin1String("type")] = RIS_DataTypeToString(rid.type);
if (rid.validMin != 0 || rid.validMax != 0)
{
result[QLatin1String("minval")] = rid.validMin;
result[QLatin1String("maxval")] = rid.validMax;
}
return result;
}
*/
QVariantMap RIS_IntrospectButtonEventItem(const ResourceItemDescriptor &rid, const Resource *r)
{
QVariantMap result = RIS_IntrospectGenericItem(rid);
Q_ASSERT(r->prefix() == RSensors);
const auto *sensor = static_cast<const Sensor*>(r);
if (!sensor)
{
return result;
}
{
const DeviceDescription &ddf = DeviceDescriptions::instance()->get(r);
if (ddf.isValid())
{
for (const DeviceDescription::SubDevice &subd : ddf.subDevices)
{
if (!subd.buttonEvents.empty())
{
QVariantMap buttons;
QVariantMap values;
for (unsigned btn: subd.buttonEvents)
{
{
QVariantMap m;
m[QLatin1String("button")] = int(btn / 1000);
m[QLatin1String("action")] = RIS_ButtonEventActionToString(btn);
values[QString::number(btn)] = m;
}
QString btnNum = QString::number(btn/1000);
if (!buttons.contains(btnNum))
{
QVariantMap m;
m[QLatin1String("name")] = QString("Button %1").arg(btn/1000);
buttons[btnNum] = m;
}
}
result[QLatin1String("buttons")] = buttons;
result[QLatin1String("values")] = values;
return result;
}
}
}
}
const deCONZ::Node *node = getCoreNode(sensor->address().ext(), deCONZ::ApsController::instance());
if (!node)
{
return result;
}
const auto &buttonMapButtons = plugin->buttonMeta;
const auto &buttonMapData = plugin->buttonMaps;
const auto &buttonMapForModelId = plugin->buttonProductMap;
const auto *buttonData = BM_ButtonMapForProduct(productHash(r), buttonMapData, buttonMapForModelId);
if (!buttonData)
{
return result;
}
int buttonBits = 0;
{
QVariantMap values;
for (const auto &btn : buttonData->buttons)
{
const auto sd = std::find_if(node->simpleDescriptors().cbegin(), node->simpleDescriptors().cend(),
[&btn](const deCONZ::SimpleDescriptor &x){ return x.endpoint() == btn.endpoint; });
if (sd == node->simpleDescriptors().cend())
{
continue;
}
buttonBits |= 1 << int(btn.button / 1000);
QVariantMap m;
m[QLatin1String("button")] = int(btn.button / 1000);
m[QLatin1String("action")] = RIS_ButtonEventActionToString(btn.button);
values[QString::number(btn.button)] = m;
}
result[QLatin1String("values")] = values;
}
const auto buttonsMeta = std::find_if(buttonMapButtons.cbegin(), buttonMapButtons.cend(),
[buttonData](const auto &meta){ return meta.buttonMapRef.hash == buttonData->buttonMapRef.hash; });
QVariantMap buttons;
if (buttonsMeta != buttonMapButtons.cend())
{
for (const auto &button : buttonsMeta->buttons)
{
if (buttonBits & (1 << button.button))
{
QVariantMap m;
AT_Atom nameAtom = AT_GetAtomByIndex({button.nameAtomeIndex});
if (nameAtom.data)
{
m[QLatin1String("name")] = QString::fromUtf8((const char*)nameAtom.data, nameAtom.len);
buttons[QString::number(button.button)] = m;
}
}
}
}
else
{
for (int i = 1 ; i < 32; i++)
{
if (buttonBits & (1 << i))
{
QVariantMap m;
m[QLatin1String("name")] = QString("Button %1").arg(i);
buttons[QString::number(i)] = m;
}
}
}
result[QLatin1String("buttons")] = buttons;
return result;
}
Fills ResourceItemDescriptor \p rid for the '[<prefix>/]<item>' part of the URL.
\note The verification that the URL has enough segments must be done by the caller.
*/
bool RIS_ResourceItemDescriptorFromHeader(const QHttpRequestHeader &hdr, ResourceItemDescriptor *rid)
{
const auto last = hdr.pathAt(hdr.pathComponentsCount() - 2);
const char *beg = hdr.pathAt(4).data();
const char *end = last.data() + last.size();
if (beg && end && beg < end)
{
const QLatin1String suffix(beg, end - beg);
if (getResourceItemDescriptor(suffix, *rid))
{
return true;
}
}
return false;
}
*/
static Resource *resourceForUniqueId(const QLatin1String &uniqueid)
{
Resource *r = plugin->getResource(RSensors, uniqueid);
if (!r)
{
plugin->getResource(RLights, uniqueid);
}
return r;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int RIS_GetDeviceItemIntrospect(const ApiRequest &req, ApiResponse &rsp)
{
rsp.httpStatus = HttpStatusOk;
const Resource *r = resourceForUniqueId(req.hdr.pathAt(3));
if (!r)
{
rsp.httpStatus = HttpStatusNotFound;
return REQ_READY_SEND;
}
ResourceItemDescriptor rid;
if (!RIS_ResourceItemDescriptorFromHeader(req.hdr, &rid))
{
rsp.httpStatus = HttpStatusNotFound;
return REQ_READY_SEND;
}
if (rid.suffix == RStateButtonEvent)
{
rsp.map = RIS_IntrospectButtonEventItem(rid, r);
}
else
{
rsp.map = RIS_IntrospectGenericItem(rid);
}
return REQ_READY_SEND;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
Adds an Zigbee 3.0 Install Code for a device to let it securely join.
Unstable API to experiment: don't use in production!
*/
int RestDevices::putDeviceInstallCode(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 5);
bool ok;
const QString &uniqueid = req.path[3];
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/installcode").arg(uniqueid), QString("body contains invalid JSON")));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
if (map.contains("installcode"))
{
std::string installCode = map["installcode"].toString().toStdString();
if (map["installcode"].type() == QVariant::String && !installCode.empty())
{
char mmoHashHex[128] = {0};
std::vector<unsigned char> mmoHash;
if (!CRYPTO_GetMmoHashFromInstallCode(installCode, mmoHash))
{
rsp.list.append(errorToMap(ERR_INTERNAL_ERROR, QLatin1String("/devices"), QLatin1String("internal error, failed to calc mmo hash, occured")));
rsp.httpStatus = HttpStatusServiceUnavailable;
return REQ_READY_SEND;
}
#if DECONZ_LIB_VERSION >= 0x010B00
QVariantMap m;
m["mac"] = uniqueid.toULongLong(&ok, 16);
if (mmoHash.size() == 16)
{
DBG_HexToAscii(&mmoHash[0], mmoHash.size(), reinterpret_cast<unsigned char*>(&mmoHashHex[0]));
}
m["key"] = QString::fromLatin1(&mmoHashHex[0]);
if (ok && strlen(mmoHashHex) == 32)
{
ok = deCONZ::ApsController::instance()->setParameter(deCONZ::ParamLinkKey, m);
}
#endif
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState["installcode"] = QString::fromLatin1(installCode.data());
rspItemState["mmohash"] = QString::fromLatin1(&mmoHashHex[0]);
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
else
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/devices"), QString("invalid value, %1, for parameter, installcode").arg(installCode.data())));
rsp.httpStatus = HttpStatusBadRequest;
}
}
else
{
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/devices/%1/installcode").arg(uniqueid), QString("missing parameters in body")));
rsp.httpStatus = HttpStatusBadRequest;
}
return REQ_READY_SEND;
}
int RestDevices::putDeviceReloadDDF(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 6);
rsp.httpStatus = HttpStatusOk;
QLatin1String uniqueId = req.hdr.pathAt(3);
DeviceKey deviceKey = getDeviceKey(uniqueId);
if (deviceKey)
{
Device *device = DEV_GetDevice(plugin->m_devices, deviceKey);
if (device)
{
DeviceDescriptions::instance()->reloadAllRawJsonAndBundles(device);
}
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState["reload"] = req.path.at(3);
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
rsp.httpStatus = HttpStatusOk;
}
else
{
}
return REQ_READY_SEND;
}
curl -X PUT -H "Content-Type: application/json" -d '{"policy": "nope", "hash":"value"}' 127.0.0.1:8090/api/12345/devices/00.99/ddf/policy
*/
int RestDevices::putDeviceSetDDFPolicy(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 6);
Device *device = nullptr;
QLatin1String uniqueId = req.hdr.pathAt(3);
DeviceKey deviceKey = getDeviceKey(uniqueId);
const QByteArray content = req.content.toUtf8();
const QString errAddr = QString("/devices/%1/ddf/policy").arg(uniqueId);
U_SStream ss;
cj_ctx cj;
std::array<cj_token, 16> tokens;
cj_token_ref refParent = 0;
cj_token_ref refPolicy;
char policyBuf[32];
char bundleHashBuf[96];
unsigned bundleHashLen = 0;
unsigned policyLen = 0;
if (deviceKey != 0)
{
device = DEV_GetDevice(plugin->m_devices, deviceKey);
}
if (!device)
{
rsp.httpStatus = HttpStatusNotFound;
rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, errAddr, QString("resource, /devices/%1, not available").arg(uniqueId)));
return REQ_READY_SEND;
}
cj_parse_init(&cj, content.data(), (cj_size)content.size(), tokens.data(), tokens.size());
cj_parse(&cj);
if (cj.status != CJ_OK)
{
rsp.list.append(errorToMap(ERR_INVALID_JSON, errAddr, "body contains invalid JSON"));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
if (cj_copy_value(&cj, policyBuf, sizeof(policyBuf), refParent, "policy") == 0)
{
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, errAddr, "missing parameters in body"));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
* Verify it's a valid policy value.
*/
policyLen = U_strlen(policyBuf);
const char *validValues[5] = { "latest_prefer_stable", "latest", "pin", "raw_json", nullptr };
U_sstream_init(&ss, policyBuf, policyLen);
int v = 0;
for (; validValues[v]; v++)
{
unsigned len = U_strlen(validValues[v]);
if (policyLen == len && U_sstream_starts_with(&ss, validValues[v]))
break;
}
if (validValues[v] == nullptr)
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, errAddr, QString("invalid value, %1, for parameter, policy").arg(policyBuf)));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
* The 'pin' policy requires a 'hash' value to be specified.
*/
if (U_sstream_starts_with(&ss, "pin"))
{
if (cj_copy_value(&cj, bundleHashBuf, sizeof(bundleHashBuf), refParent, "hash") == 0)
{
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, errAddr, "missing parameters in body"));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
bundleHashLen = U_strlen(bundleHashBuf);
if (!DDFB_SanitizeBundleHashString(bundleHashBuf, bundleHashLen))
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, errAddr, QString("invalid value, %1, for parameter, hash").arg(bundleHashBuf)));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
}
bool needReload = false;
ResourceItem *ddfPolicyItem = device->item(RAttrDdfPolicy);
ResourceItem *ddfHashItem = device->item(RAttrDdfHash);
U_ASSERT(ddfPolicyItem);
U_ASSERT(ddfHashItem);
if (!ddfPolicyItem->equalsString(policyBuf, policyLen))
{
ddfPolicyItem->setValue(policyBuf, policyLen, ResourceItem::SourceApi);
needReload = true;
DB_ResourceItem2 dbItem;
dbItem.name = RAttrDdfPolicy;
U_memcpy(dbItem.value, policyBuf, policyLen);
dbItem.value[policyLen] = '\0';
dbItem.valueSize = policyLen;
dbItem.timestampMs = ddfPolicyItem->lastSet().toMSecsSinceEpoch();
DB_StoreDeviceItem(device->deviceId(), dbItem);
}
if (bundleHashLen != 0 && !ddfHashItem->equalsString(bundleHashBuf, bundleHashLen))
{
ddfHashItem->setValue(bundleHashBuf, bundleHashLen, ResourceItem::SourceApi);
needReload = true;
DB_ResourceItem2 dbItem;
dbItem.name = RAttrDdfHash;
U_memcpy(dbItem.value, bundleHashBuf, bundleHashLen);
dbItem.value[bundleHashLen] = '\0';
dbItem.valueSize = bundleHashLen;
dbItem.timestampMs = ddfHashItem->lastSet().toMSecsSinceEpoch();
DB_StoreDeviceItem(device->deviceId(), dbItem);
}
rsp.httpStatus = HttpStatusOk;
{
QVariantMap result;
QVariantMap item;
item[QString("/devices/%1/ddf/policy").arg(uniqueId)] = QString::fromLatin1(policyBuf);
result["success"] = item;
rsp.list.append(result);
}
if (bundleHashLen != 0)
{
QVariantMap result;
QVariantMap item;
item[QString("/devices/%1/ddf/hash").arg(uniqueId)] = QString::fromLatin1(bundleHashBuf);
result["success"] = item;
rsp.list.append(result);
}
if (needReload)
{
emit eventNotify(Event(RDevices, REventDDFReload, 0, deviceKey));
}
return REQ_READY_SEND;
}