* Copyright (c) 2021-2024 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 "deconz/u_assert.h"
#include "device.h"
#include "device_compat.h"
#include "device_descriptions.h"
#include "device_ddf_init.h"
#include "deconz/u_sstream_ex.h"
#include "database.h"
#include "poll_control.h"
#include "utils/utils.h"
void DEV_AllocateGroup(const Device *device, Resource *rsub, ResourceItem *item);
static QString uniqueIdFromTemplate(const QStringList &templ, const Device *device)
{
bool ok = false;
quint8 endpoint = 0;
quint16 clusterId = 0;
quint64 extAddress;
extAddress = device->item(RAttrExtAddress)->toNumber();
["$address.ext", <endpoint>]
["$address.ext", <endpoint>, <cluster>]
*/
if (templ.size() > 1 && templ.first() == QLatin1String("$address.ext"))
{
endpoint = templ.at(1).toUInt(&ok, 0);
if (ok && templ.size() > 2)
{
ok = false;
const QString pos2 = templ.at(2);
if (pos2.at(0).isDigit())
{
clusterId = pos2.toUInt(&ok, 0);
}
}
}
if (ok)
{
return generateUniqueId(extAddress, endpoint, clusterId);
}
return {};
}
*/
static ResourceItem *DEV_InitDeviceDescriptionItem(const DeviceDescription::Item &ddfItem, const std::vector<DB_ResourceItem> &dbItems, Resource *rsub)
{
U_ASSERT(rsub);
U_ASSERT(ddfItem.isValid());
auto *item = rsub->item(ddfItem.descriptor.suffix);
const char *uniqueId = rsub->item(RAttrUniqueId)->toCString();
U_ASSERT(uniqueId);
if (item)
{
if (DBG_IsEnabled(DBG_INFO_L2))
{
DBG_Printf(DBG_DDF, "sub-device: %s, has item: %s\n", uniqueId, ddfItem.descriptor.suffix);
}
}
else
{
if (DBG_IsEnabled(DBG_INFO_L2))
{
DBG_Printf(DBG_DDF, "sub-device: %s, create item: %s\n", uniqueId, ddfItem.descriptor.suffix);
}
item = rsub->addItem(ddfItem.descriptor.type, ddfItem.descriptor.suffix);
DBG_Assert(item);
if (!item)
{
return nullptr;
}
}
U_ASSERT(item);
const auto dbItem = std::find_if(dbItems.cbegin(), dbItems.cend(), [&ddfItem](const auto &dbItem)
{
return ddfItem.name == dbItem.name;
});
if (!ddfItem.isStatic && dbItem != dbItems.cend())
{
if (item->descriptor().suffix == RAttrId && !item->toString().isEmpty())
{
}
else if (item->lastSet().isValid() && item->toVariant() == dbItem->value)
{
}
else
{
item->setValue(dbItem->value);
item->setTimeStamps(QDateTime::fromMSecsSinceEpoch(dbItem->timestampMs));
}
item->clearNeedStore();
}
else if (!ddfItem.isStatic && dbItem == dbItems.cend() && !item->lastSet().isValid())
{
auto dbLegacyItem = std::make_unique<DB_LegacyItem>();
dbLegacyItem->uniqueId = uniqueId;
dbLegacyItem->column.setString(item->descriptor().suffix);
if (rsub->prefix() == RSensors)
{
DB_LoadLegacySensorValue(dbLegacyItem.get());
}
if (!dbLegacyItem->value.empty())
{
item->setValue(QVariant(QString(dbLegacyItem->value.c_str())));
item->setTimeStamps(item->lastSet().addSecs(-120));
}
}
if (ddfItem.defaultValue.isValid())
{
if (ddfItem.isStatic || !item->lastSet().isValid())
{
item->setValue(ddfItem.defaultValue);
item->setTimeStamps(item->lastSet().addSecs(-86400));
item->clearNeedStore();
}
}
U_ASSERT(ddfItem.handle != DeviceDescription::Item::InvalidItemHandle);
item->setDdfItemHandle(ddfItem.handle);
item->setIsPublic(ddfItem.isPublic);
item->setAwake(ddfItem.awake);
if (ddfItem.refreshInterval != DeviceDescription::Item::NoRefreshInterval)
{
item->setRefreshInterval(deCONZ::TimeSeconds{ddfItem.refreshInterval});
}
if (item->refreshInterval().val == 0 && !ddfItem.readParameters.isNull())
{
const auto m = ddfItem.readParameters.toMap();
if (m.value(QLatin1String("fn")) != QLatin1String("none"))
{
item->setRefreshInterval(deCONZ::TimeSeconds{30});
}
}
item->setParseFunction(nullptr);
return item;
}
This function replaces the legacy database loading and joining device initialization.
*/
bool DEV_InitDeviceFromDescription(Device *device, const DeviceDescription &ddf)
{
Q_ASSERT(device);
Q_ASSERT(ddf.isValid());
size_t subCount = 0;
auto *dd = DeviceDescriptions::instance();
if (ddf.storageLocation == deCONZ::DdfBundleLocation || ddf.storageLocation == deCONZ::DdfBundleUserLocation)
{
ResourceItem *ddfhashItem = device->item(RAttrDdfHash);
U_ASSERT(ddfhashItem);
char buf[72];
U_SStream ss;
U_sstream_init(&ss, buf, sizeof(buf));
U_sstream_put_hex(&ss, &ddf.sha256Hash[0], sizeof(ddf.sha256Hash));
for (unsigned i = 0; i < ss.pos; i++)
{
if (buf[i] >= 'A' && buf[i] <= 'F')
buf[i] = buf[i] + ('a' - 'A');
}
U_ASSERT(ss.pos == 64);
ddfhashItem->setValue(buf, 64);
}
for (const auto &sub : ddf.subDevices)
{
Q_ASSERT(sub.isValid());
const auto uniqueId = uniqueIdFromTemplate(sub.uniqueId, device);
if (uniqueId.isEmpty())
{
DBG_Printf(DBG_DDF, "failed to init sub-device uniqueid: %s, %s\n", qPrintable(sub.uniqueId.join('-')), qPrintable(sub.type));
return false;
}
Resource *rsub = DEV_GetSubDevice(device, nullptr, uniqueId);
if (!rsub)
{
rsub = DEV_InitCompatNodeFromDescription(device, ddf, sub, uniqueId);
}
if (!rsub)
{
DBG_Printf(DBG_DDF, "sub-device: %s, failed to setup: %s\n", qPrintable(uniqueId), qPrintable(sub.type));
return false;
}
subCount++;
auto *mf = rsub->item(RAttrManufacturerName);
if (mf && mf->toLatin1String().size() == 0)
{
mf->setValue(dd->constantToString(device->item(RAttrManufacturerName)->toString()));
}
DB_StoreSubDevice(rsub->item(RAttrUniqueId)->toCString());
const auto dbItems = DB_LoadSubDeviceItems(rsub->item(RAttrUniqueId)->toLatin1String());
for (const auto &ddfItem : sub.items)
{
auto *item = DEV_InitDeviceDescriptionItem(ddfItem, dbItems, rsub);
if (!item)
{
continue;
}
if (item->descriptor().suffix == RStatePresence && item->toBool())
{
DBG_Printf(DBG_DDF, "sub-device: %s, presence state is true, reverting to false\n", qPrintable(uniqueId));
item->setValue(false);
item->clearNeedStore();
}
if (item->descriptor().suffix == RConfigGroup)
{
DEV_AllocateGroup(device, rsub, item);
}
if (!ddfItem.defaultValue.isNull() && !ddfItem.writeParameters.isNull())
{
QString writeFunction;
const auto writeParam = ddfItem.writeParameters.toMap();
if (writeParam.contains(QLatin1String("fn")))
{
writeFunction = writeParam.value(QLatin1String("fn")).toString();
}
if (writeFunction.isEmpty() || writeFunction.startsWith(QLatin1String("zcl")))
{
bool ok;
QVariant value = item->toVariant();
if (!value.isValid())
{
value = ddfItem.defaultValue;
}
StateChange stateChange(StateChange::StateWaitSync, SC_WriteZclAttribute, sub.uniqueId.at(1).toUInt());
stateChange.addTargetValue(item->descriptor().suffix, value);
stateChange.setChangeTimeoutMs(1000 * 60 * 60);
if (writeParam.contains(QLatin1String("state.timeout")))
{
int stateTimeout = writeParam.value(QLatin1String("state.timeout")).toInt(&ok);
if (ok && stateTimeout > 0)
{
stateChange.setStateTimeoutMs(1000 * stateTimeout);
}
}
rsub->addStateChange(stateChange);
}
}
if (item->descriptor().suffix == RAttrType)
{
const QString type = DeviceDescriptions::instance()->constantToString(sub.type);
if (type != item->toString() && !type.startsWith('$'))
{
item->setValue(type);
}
}
if (item->descriptor().suffix == RConfigBattery || item->descriptor().suffix == RStateBattery)
{
DEV_ForwardNodeChange(device, QLatin1String(item->descriptor().suffix), QString::number(item->toNumber()));
}
if (item->descriptor().suffix == RConfigCheckin)
{
if (PC_GetPollControlEndpoint(device->node()) > 0)
{
auto *itemPending = rsub->item(RConfigPending);
if (itemPending)
{
itemPending->setValue(itemPending->toNumber() | R_PENDING_SET_LONG_POLL_INTERVAL);
}
}
}
}
DB_StoreSubDeviceItem(rsub, rsub->item(RAttrManufacturerName));
DB_StoreSubDeviceItem(rsub, rsub->item(RAttrModelId));
}
if (ddf.sleeper >= 0)
{
device->item(RCapSleeper)->setValue(ddf.sleeper == 1);
}
if (ddf.supportsMgmtBind >= 0)
{
device->setSupportsMgmtBind(ddf.supportsMgmtBind == 1);
}
device->clearBindings();
for (const auto &bnd : ddf.bindings)
{
device->addBinding(bnd);
}
return subCount == ddf.subDevices.size();
}
bool DEV_InitBaseDescriptionForDevice(Device *device, DeviceDescription &ddf)
{
ddf.status = QLatin1String("Draft");
ddf.manufacturerNames.push_back(device->item(RAttrManufacturerName)->toString());
ddf.modelIds.push_back(device->item(RAttrModelId)->toString());
if (ddf.manufacturerNames.last().isEmpty() || ddf.modelIds.isEmpty() || ddf.modelIds.front().isEmpty())
{
return false;
}
const auto *dd = DeviceDescriptions::instance();
for (const Resource *r : device->subDevices())
{
DeviceDescription::SubDevice sub;
sub.type = dd->stringToConstant(r->item(RAttrType)->toString());
sub.restApi = QLatin1String(r->prefix());
if (ddf.product.isEmpty())
{
const ResourceItem *productId = r->item(RAttrProductId);
if (productId && !productId->toString().isEmpty())
{
ddf.product = productId->toString();
}
}
{
QString uniqueId = r->item(RAttrUniqueId)->toString();
QStringList ls = uniqueId.split('-', SKIP_EMPTY_PARTS);
DBG_Assert(ls.size() > 1);
if (ls.size() > 1)
{
ls[0] = QLatin1String("$address.ext");
ls[1] = "0x" + ls[1];
if (ls.size() > 2)
{
ls[2] = "0x" + ls[2];
}
sub.uniqueId = ls;
}
else
{
return false;
}
}
if (r->prefix() == RSensors)
{
const Sensor *sensor = dynamic_cast<const Sensor*>(r);
if (sensor)
{
sub.fingerPrint = sensor->fingerPrint();
}
}
for (int i = 0; i < r->itemCount(); i++)
{
const ResourceItem *item = r->itemForIndex(i);
DeviceDescription::Item ddfItem = dd->getGenericItem(item->descriptor().suffix);
if (!ddfItem.isValid())
{
ddfItem.name = item->descriptor().suffix;
ddfItem.descriptor = item->descriptor();
}
ddfItem.isPublic = item->isPublic();
sub.items.push_back(ddfItem);
}
ddf.subDevices.push_back(sub);
}
return true;
}
bool DEV_InitDeviceBasic(Device *device)
{
{
ResourceItem *ddfPolicy = device->item(RAttrDdfPolicy);
U_ASSERT(ddfPolicy);
{
std::vector<DB_ResourceItem2> dbItems2;
if (DB_LoadDeviceItems(device->deviceId(), dbItems2))
{
for (const auto &dbItem : dbItems2)
{
U_ASSERT(dbItem.valueSize != 0);
if (dbItem.valueSize == 0)
{
continue;
}
if (dbItem.name == RAttrDdfPolicy && ddfPolicy)
{
ddfPolicy->setValue(dbItem.value, (int)dbItem.valueSize);
ddfPolicy->setTimeStamps(QDateTime::fromMSecsSinceEpoch(dbItem.timestampMs));
ddfPolicy->clearNeedStore();
}
else if (dbItem.name == RAttrDdfHash)
{
U_ASSERT(dbItem.valueSize == 64);
if (dbItem.valueSize == 64)
{
ResourceItem *ddfHash = device->item(RAttrDdfHash);
ddfHash->setValue(dbItem.value, (int)dbItem.valueSize);
ddfHash->setTimeStamps(QDateTime::fromMSecsSinceEpoch(dbItem.timestampMs));
ddfHash->clearNeedStore();
}
}
}
}
}
if (ddfPolicy && ddfPolicy->toLatin1String().size() == 0)
{
ddfPolicy->setValue("latest_prefer_stable", -1);
}
}
const auto dbItems = DB_LoadSubDeviceItemsOfDevice(device->item(RAttrUniqueId)->toLatin1String());
size_t found = 0;
std::array<const char*, 2> poi = { RAttrManufacturerName, RAttrModelId };
for (const auto &dbItem : dbItems)
{
if (dbItem.name == RStateReachable || dbItem.name == RConfigReachable)
{
ResourceItem *reachable = device->item(RStateReachable);
DBG_Assert(reachable);
if (reachable)
{
if (dbItem.value.toBool())
{
auto dt = QDateTime::fromMSecsSinceEpoch(dbItem.timestampMs);
if (86400 < dt.secsTo(QDateTime::currentDateTime()))
{
reachable->setValue(false);
continue;
}
}
reachable->setValue(dbItem.value.toBool());
reachable->setTimeStamps(QDateTime::fromMSecsSinceEpoch(dbItem.timestampMs));
reachable->clearNeedStore();
}
continue;
}
for (const char *suffix: poi)
{
if (dbItem.name != suffix)
{
continue;
}
auto *item = device->item(suffix);
if (item)
{
item->setValue(dbItem.value);
item->setTimeStamps(QDateTime::fromMSecsSinceEpoch(dbItem.timestampMs));
item->clearNeedStore();
found++;
}
break;
}
}
DB_ZclValue zclVal;
zclVal.deviceId = device->deviceId();
zclVal.endpoint = 0;
zclVal.clusterId = 0x0019;
zclVal.attrId = 0x0002;
zclVal.data = 0;
if (DB_LoadZclValue(&zclVal) && zclVal.data != 0)
{
ResourceItem *item = device->item(RAttrOtaVersion);
if (item && item->toNumber() != zclVal.data)
{
item->setValue(zclVal.data, ResourceItem::SourceDevice);
item->clearNeedPush();
}
}
zclVal.clusterId = 0x0500;
zclVal.attrId = 0x0001;
zclVal.data = 0;
if (DB_LoadZclValue(&zclVal) && zclVal.data != 0)
{
ResourceItem *item = device->addItem(DataTypeUInt16, RAttrZoneType);
if (item && item->toNumber() != zclVal.data)
{
item->setValue(zclVal.data, ResourceItem::SourceDevice);
item->clearNeedPush();
}
}
return found == poi.size();
}