/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2020, 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 General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU General Public License as published by the Free Software * Foundation, GNU 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 General * Public License for more details. * * You should have received a copy of the GNU 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 "thingmanagerimplementation.h" #include "translator.h" #if QT_VERSION >= QT_VERSION_CHECK(5,12,0) #include "scriptintegrationplugin.h" #endif #include "loggingcategories.h" #include "typeutils.h" #include "nymeasettings.h" #include "version.h" #include "plugininfocache.h" #include "integrations/thingdiscoveryinfo.h" #include "integrations/thingpairinginfo.h" #include "integrations/thingsetupinfo.h" #include "integrations/thingactioninfo.h" #include "integrations/integrationplugin.h" #include "integrations/thingutils.h" #include "integrations/browseresult.h" #include "integrations/browseritemresult.h" #include "integrations/browseractioninfo.h" #include "integrations/browseritemactioninfo.h" //#include "unistd.h" #include "plugintimer.h" #include #include #include #include #include #include #include #include ThingManagerImplementation::ThingManagerImplementation(HardwareManager *hardwareManager, const QLocale &locale, QObject *parent) : ThingManager(parent), m_hardwareManager(hardwareManager), m_locale(locale), m_translator(new Translator(this)) { qRegisterMetaType(); qRegisterMetaType(); foreach (const Interface &interface, ThingUtils::allInterfaces()) { m_supportedInterfaces.insert(interface.name(), interface); } // Migrate config from devices.conf (<0.20) to things.conf QString settingsPath = NymeaSettings::settingsPath(); if (QFile::exists(settingsPath + "/devices.conf") && !QFile::exists(settingsPath + "/things.conf")) { qCDebug(dcThingManager()) << "Migrating config from devices.conf to things.conf"; QFile oldFile(settingsPath + "/devices.conf"); oldFile.copy(settingsPath + "/things.conf"); QFile oldStateFile(settingsPath + "/devicestates.conf"); oldStateFile.copy(settingsPath + "/thingstates.conf"); } // Give hardware a chance to start up before loading plugins etc. QMetaObject::invokeMethod(this, "loadPlugins", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "loadConfiguredThings", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "startMonitoringAutoThings", Qt::QueuedConnection); // Make sure this is always emitted after plugins and things are loaded QMetaObject::invokeMethod(this, "onLoaded", Qt::QueuedConnection); } ThingManagerImplementation::~ThingManagerImplementation() { delete m_translator; foreach (Thing *thing, m_configuredThings) { storeThingStates(thing); delete thing; } foreach (IntegrationPlugin *plugin, m_integrationPlugins) { if (plugin->parent() == this) { qCDebug(dcThingManager()) << "Deleting plugin" << plugin->pluginName(); delete plugin; } else { qCDebug(dcThingManager()) << "Not deleting plugin" << plugin->pluginName(); } } } QStringList ThingManagerImplementation::pluginSearchDirs() { QStringList searchDirs; QByteArray envPath = qgetenv("NYMEA_PLUGINS_PATH"); if (!envPath.isEmpty()) { searchDirs << QString(envPath).split(':'); } foreach (QString libraryPath, QCoreApplication::libraryPaths()) { searchDirs << libraryPath.replace("qt5", "nymea"); } foreach (QString libraryPath, QCoreApplication::libraryPaths()) { searchDirs << libraryPath.replace("plugins", "nymea/plugins"); } searchDirs << QDir(QCoreApplication::applicationDirPath() + "/../lib/nymea/plugins/").absolutePath(); searchDirs << QDir(QCoreApplication::applicationDirPath() + "/../plugins/").absolutePath(); searchDirs << QDir(QCoreApplication::applicationDirPath() + "/../../../plugins/").absolutePath(); searchDirs.removeDuplicates(); return searchDirs; } QList ThingManagerImplementation::pluginsMetadata() { QList pluginList; foreach (const QString &path, pluginSearchDirs()) { QDir dir(path); foreach (const QString &entry, dir.entryList()) { QFileInfo fi; if (entry.startsWith("libnymea_integrationplugin") && entry.endsWith(".so")) { fi.setFile(path + "/" + entry); } else { fi.setFile(path + "/" + entry + "/libnymea_integrationplugin" + entry + ".so"); } if (!fi.exists()) { continue; } QPluginLoader loader(fi.absoluteFilePath()); pluginList.append(loader.metaData().value("MetaData").toObject()); } } return pluginList; } void ThingManagerImplementation::registerStaticPlugin(IntegrationPlugin *plugin, const PluginMetadata &metaData) { if (!metaData.isValid()) { qCWarning(dcThingManager()) << "Plugin metadata not valid. Not loading static plugin:" << plugin->pluginName(); return; } loadPlugin(plugin, metaData); } IntegrationPlugins ThingManagerImplementation::plugins() const { return m_integrationPlugins.values(); } IntegrationPlugin *ThingManagerImplementation::plugin(const PluginId &pluginId) const { return m_integrationPlugins.value(pluginId); } Thing::ThingError ThingManagerImplementation::setPluginConfig(const PluginId &pluginId, const ParamList &pluginConfig) { IntegrationPlugin *plugin = m_integrationPlugins.value(pluginId); if (!plugin) { qCWarning(dcThingManager()) << "Could not set plugin configuration. There is no plugin with id" << pluginId.toString(); return Thing::ThingErrorPluginNotFound; } ParamList params = buildParams(plugin->configurationDescription(), pluginConfig); Thing::ThingError verify = ThingUtils::verifyParams(plugin->configurationDescription(), params); if (verify != Thing::ThingErrorNoError) return verify; Thing::ThingError result = plugin->setConfiguration(params); if (result != Thing::ThingErrorNoError) return result; NymeaSettings settings(NymeaSettings::SettingsRolePlugins); settings.beginGroup("PluginConfig"); settings.beginGroup(plugin->pluginId().toString()); foreach (const Param ¶m, pluginConfig) { settings.beginGroup(param.paramTypeId().toString()); settings.setValue("type", static_cast(param.value().type())); settings.setValue("value", param.value()); settings.endGroup(); } settings.endGroup(); settings.endGroup(); emit pluginConfigChanged(plugin->pluginId(), pluginConfig); return result; } Vendors ThingManagerImplementation::supportedVendors() const { return m_supportedVendors.values(); } Interfaces ThingManagerImplementation::supportedInterfaces() const { return m_supportedInterfaces.values(); } ThingClasses ThingManagerImplementation::supportedThings(const VendorId &vendorId) const { if (vendorId.isNull()) { return m_supportedThings.values(); } QList ret; foreach (const ThingClass &thingClass, m_supportedThings) { if (!vendorId.isNull() && thingClass.vendorId() != vendorId) { continue; } ret.append(thingClass); } return ret; } ThingDiscoveryInfo* ThingManagerImplementation::discoverThings(const ThingClassId &thingClassId, const ParamList ¶ms) { ThingClass thingClass = findThingClass(thingClassId); if (!thingClass.isValid()) { qCWarning(dcThingManager) << "Thing discovery failed. Invalid thing class id:" << thingClassId.toString(); ThingDiscoveryInfo *discoveryInfo = new ThingDiscoveryInfo(thingClassId, params, this); discoveryInfo->finish(Thing::ThingErrorThingClassNotFound); return discoveryInfo; } if (!thingClass.createMethods().testFlag(ThingClass::CreateMethodDiscovery)) { qCWarning(dcThingManager) << "Thing discovery failed. Thing class" << thingClass.name() << "cannot be discovered."; ThingDiscoveryInfo *discoveryInfo = new ThingDiscoveryInfo(thingClassId, params, this); discoveryInfo->finish(Thing::ThingErrorCreationMethodNotSupported); return discoveryInfo; } IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); if (!plugin) { qCWarning(dcThingManager) << "Thing discovery failed. Plugin not found for thing class" << thingClass.name(); ThingDiscoveryInfo *discoveryInfo = new ThingDiscoveryInfo(thingClassId, params, this); discoveryInfo->finish(Thing::ThingErrorPluginNotFound, tr("The plugin for this thing is not loaded.")); return discoveryInfo; } ParamList effectiveParams = buildParams(thingClass.discoveryParamTypes(), params); Thing::ThingError result = ThingUtils::verifyParams(thingClass.discoveryParamTypes(), effectiveParams); if (result != Thing::ThingErrorNoError) { qCWarning(dcThingManager) << "Thing discovery failed. Parameter verification failed."; ThingDiscoveryInfo *discoveryInfo = new ThingDiscoveryInfo(thingClassId, params, this); discoveryInfo->finish(result); return discoveryInfo; } ThingDiscoveryInfo *discoveryInfo = new ThingDiscoveryInfo(thingClassId, effectiveParams, this, 30000); connect(discoveryInfo, &ThingDiscoveryInfo::finished, this, [this, discoveryInfo](){ if (discoveryInfo->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Discovery failed:" << discoveryInfo->status() << discoveryInfo->displayMessage(); return; } qCDebug(dcThingManager()) << "Discovery finished. Found things:" << discoveryInfo->thingDescriptors().count(); foreach (const ThingDescriptor &descriptor, discoveryInfo->thingDescriptors()) { if (!descriptor.isValid()) { qCWarning(dcThingManager()) << "Descriptor is invalid. Not adding to results"; continue; } m_discoveredThings.insert(descriptor.id(), descriptor); } }); qCDebug(dcThingManager) << "Thing discovery for" << thingClass.name() << "started..."; plugin->discoverThings(discoveryInfo); return discoveryInfo; } ThingSetupInfo* ThingManagerImplementation::addConfiguredThing(const ThingClassId &thingClassId, const ParamList ¶ms, const QString &name) { return addConfiguredThingInternal(thingClassId, name, params); } ThingSetupInfo *ThingManagerImplementation::addConfiguredThing(const ThingDescriptorId &thingDescriptorId, const ParamList ¶ms, const QString &name) { ThingDescriptor descriptor = m_discoveredThings.value(thingDescriptorId); if (!descriptor.isValid()) { qCWarning(dcThingManager()) << "Cannot add thing. ThingDescriptor" << thingDescriptorId << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingDescriptorNotFound); return info; } ThingClass thingClass = findThingClass(descriptor.thingClassId()); if (!thingClass.isValid()) { qCWarning(dcThingManager()) << "Cannot add thing. ThingClass" << descriptor.thingClassId() << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingClassNotFound); return info; } if (!thingClass.createMethods().testFlag(ThingClass::CreateMethodDiscovery)) { qCWarning(dcThingManager()) << "Cannot add thing. This thing cannot be added via discovery."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorCreationMethodNotSupported); return info; } // Merging params from descriptor and user provided ones ParamList finalParams = buildParams(thingClass.paramTypes(), params, descriptor.params()); return addConfiguredThingInternal(descriptor.thingClassId(), name, finalParams, descriptor.parentId()); } ThingSetupInfo* ThingManagerImplementation::reconfigureThing(const ThingId &thingId, const ParamList ¶ms, const QString &name) { Thing *thing = findConfiguredThing(thingId); if (!thing) { qCWarning(dcThingManager()) << "Cannot reconfigure thing. Thing with id" << thingId.toString() << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingNotFound); return info; } ThingClass thingClass = findThingClass(thing->thingClassId()); if (thingClass.id().isNull()) { qCWarning(dcThingManager()) << "Cannot reconfigure thing. ThingClass for thing" << thing->name() << thingId.toString() << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingClassNotFound); return info; } foreach (const ParamType ¶mType, thingClass.paramTypes()) { if (paramType.readOnly() && params.hasParam(paramType.id())) { ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); qCWarning(dcThingManager()) << "Parameter" << paramType.name() << paramType.id() << "is not writable"; info->finish(Thing::ThingErrorParameterNotWritable); return info; } } ParamList finalParams = buildParams(thingClass.paramTypes(), params); return reconfigureThingInternal(thing, finalParams, name); } ThingSetupInfo *ThingManagerImplementation::reconfigureThing(const ThingDescriptorId &thingDescriptorId, const ParamList ¶ms, const QString &name) { ThingDescriptor descriptor = m_discoveredThings.value(thingDescriptorId); if (!descriptor.isValid()) { qCWarning(dcThingManager()) << "Cannot reconfigure thing. No thing descriptor with ID" << thingDescriptorId << "found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingDescriptorNotFound); return info; } Thing *thing = findConfiguredThing(descriptor.thingId()); if (!thing) { ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); qCWarning(dcThingManager()) << "Cannot reconfigure thing. No thing with ID" << descriptor.thingId() << "found."; info->finish(Thing::ThingErrorThingNotFound); return info; } ThingClass thingClass = findThingClass(thing->thingClassId()); if (!thingClass.isValid()) { qCWarning(dcThingManager()) << "Cannot reconfigure tning. No ThingClass with ID" << thing->thingClassId() << "found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingClassNotFound); return info; } ParamList finalParams = buildParams(thing->thingClass().paramTypes(), params, descriptor.params()); return reconfigureThingInternal(thing, finalParams, name); } ThingSetupInfo *ThingManagerImplementation::reconfigureThingInternal(Thing *thing, const ParamList ¶ms, const QString &name) { IntegrationPlugin *plugin = m_integrationPlugins.value(thing->thingClass().pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot reconfigure thing. Plugin for ThingClass" << thing->thingClassId().toString() << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorPluginNotFound); return info; } ParamList finalParams = buildParams(thing->thingClass().paramTypes(), params); Thing::ThingError result = ThingUtils::verifyParams(thing->thingClass().paramTypes(), finalParams); if (result != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Cannot reconfigure thing. Params failed validation."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(result); return info; } // first remove the thing in the plugin plugin->thingRemoved(thing); // mark setup as incomplete thing->setSetupStatus(Thing::ThingSetupStatusInProgress, Thing::ThingErrorNoError); // set new params foreach (const Param ¶m, params) { thing->setParamValue(param.paramTypeId(), param.value()); } if (!name.isEmpty()) { thing->setName(name); } // try to setup the thing with the new params ThingSetupInfo *info = new ThingSetupInfo(thing, this, 30000); plugin->setupThing(info); connect(info, &ThingSetupInfo::finished, this, [this, info](){ if (info->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Thing reconfiguration failed for" << info->thing()->name() << info->thing()->id().toString() << info->status() << info->displayMessage(); info->thing()->setSetupStatus(Thing::ThingSetupStatusFailed, info->status(), info->displayMessage()); // TODO: recover old params.?? return; } storeConfiguredThings(); postSetupThing(info->thing()); info->thing()->setSetupStatus(Thing::ThingSetupStatusComplete, Thing::ThingErrorNoError); emit thingChanged(info->thing()); }); return info; } Thing::ThingError ThingManagerImplementation::editThing(const ThingId &thingId, const QString &name) { Thing *thing = findConfiguredThing(thingId); if (!thing) return Thing::ThingErrorThingNotFound; thing->setName(name); return Thing::ThingErrorNoError; } Thing::ThingError ThingManagerImplementation::setThingSettings(const ThingId &thingId, const ParamList &settings) { Thing *thing = findConfiguredThing(thingId); if (!thing) { qCWarning(dcThingManager()) << "Cannot set thing settings. Thing" << thingId.toString() << "not found"; return Thing::ThingErrorThingNotFound; } // build a list of settings using: a) provided new settings b) previous settings and c) default values ParamList effectiveSettings = buildParams(thing->thingClass().settingsTypes(), settings, thing->settings()); Thing::ThingError status = ThingUtils::verifyParams(thing->thingClass().settingsTypes(), effectiveSettings); if (status != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Error setting thing settings for" << thing->name() << thing->id().toString(); return status; } thing->setSettings(effectiveSettings); return Thing::ThingErrorNoError; } ThingPairingInfo* ThingManagerImplementation::pairThing(const ThingClassId &thingClassId, const ParamList ¶ms, const QString &name) { PairingTransactionId transactionId = PairingTransactionId::createPairingTransactionId(); ThingClass thingClass = m_supportedThings.value(thingClassId); if (!thingClass.isValid()) { qCWarning(dcThingManager) << "Cannot find a ThingClass with ID" << thingClassId.toString(); ThingPairingInfo *info = new ThingPairingInfo(transactionId, thingClassId, ThingId(), name, ParamList(), ThingId(), this); info->finish(Thing::ThingErrorThingClassNotFound); return info; } // Create new thing id ThingId newThingId = ThingId::createThingId(); // Use given params, if there are missing some, use the defaults ones. ParamList finalParams = buildParams(thingClass.paramTypes(), params); ThingPairingInfo *info = new ThingPairingInfo(transactionId, thingClassId, newThingId, name, finalParams, ThingId(), this, 30000); pairThingInternal(info); return info; } ThingPairingInfo* ThingManagerImplementation::pairThing(const ThingDescriptorId &thingDescriptorId, const ParamList ¶ms, const QString &name) { PairingTransactionId pairingTransactionId = PairingTransactionId::createPairingTransactionId(); ThingDescriptor descriptor = m_discoveredThings.value(thingDescriptorId); if (!descriptor.isValid()) { qCWarning(dcThingManager) << "Cannot find a ThingDescriptor with ID" << thingDescriptorId.toString(); ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, ThingClassId(), ThingId(), name, ParamList(), ThingId(), this); info->finish(Thing::ThingErrorThingDescriptorNotFound); return info; } ThingClass thingClass = m_supportedThings.value(descriptor.thingClassId()); if (!thingClass.isValid()) { qCWarning(dcThingManager) << "Cannot find a ThingClass with ID" << descriptor.thingClassId().toString(); ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, descriptor.thingClassId(), ThingId(), name, ParamList(), ThingId(), this); info->finish(Thing::ThingErrorThingClassNotFound); return info; } ThingId thingId = descriptor.thingId(); // If it's a new thing (not a reconfiguration), create a new ThingId now. if (thingId.isNull()) { thingId = ThingId::createThingId(); } // Use given params, if there are missing some, use the discovered ones. ParamList finalParams = buildParams(thingClass.paramTypes(), params, descriptor.params()); ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, descriptor.thingClassId(), thingId, name, finalParams, descriptor.parentId(), this, 30000); pairThingInternal(info); return info; } ThingPairingInfo *ThingManagerImplementation::pairThing(const ThingId &thingId, const ParamList ¶ms, const QString &name) { PairingTransactionId pairingTransactionId = PairingTransactionId::createPairingTransactionId(); Thing *thing = findConfiguredThing(thingId); if (!thing) { qCWarning(dcThingManager) << "Cannot find a thing with ID" << thingId.toString(); ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, ThingClassId(), thingId, name, ParamList(), ThingId(), this); info->finish(Thing::ThingErrorThingDescriptorNotFound); return info; } // Use new params, if there are missing some, use the existing ones. ParamList finalParams = buildParams(thing->thingClass().paramTypes(), params, thing->params()); ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, thing->thingClassId(), thingId, name, finalParams, ThingId(), this, 30000); pairThingInternal(info); return info; } ThingPairingInfo *ThingManagerImplementation::confirmPairing(const PairingTransactionId &pairingTransactionId, const QString &username, const QString &secret) { if (!m_pendingPairings.contains(pairingTransactionId)) { qCWarning(dcThingManager()) << "No pairing transaction with id" << pairingTransactionId.toString(); ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, ThingClassId(), ThingId(), QString(), ParamList(), ThingId(), this); info->finish(Thing::ThingErrorPairingTransactionIdNotFound); return info; } PairingContext context = m_pendingPairings.take(pairingTransactionId); ThingClassId thingClassId = context.thingClassId; ThingClass thingClass = m_supportedThings.value(thingClassId); IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); if (!plugin) { qCWarning(dcThingManager) << "Can't find a plugin for this thing class:" << thingClass; ThingPairingInfo *info = new ThingPairingInfo(pairingTransactionId, thingClassId, context.thingId, context.thingName, context.params, context.parentId, this); info->finish(Thing::ThingErrorPluginNotFound); return info; } ThingId thingId = context.thingId; // If we already have a thing for this ID, we're reconfiguring an existing thing, else we're adding a new one. bool addNewThing = !m_configuredThings.contains(context.thingId); // We're using two different info objects here, one to hand over to the plugin for the pairing, the other we give out // to the user. After the internal one has finished, we'll start a setupThing job and finish the external pairingInfo only after // both, the internal pairing and the setup have completed. ThingPairingInfo *internalInfo = new ThingPairingInfo(pairingTransactionId, thingClassId, thingId, context.thingName, context.params, context.parentId, this); ThingPairingInfo *externalInfo = new ThingPairingInfo(pairingTransactionId, thingClassId, thingId, context.thingName, context.params, context.parentId, this); plugin->confirmPairing(internalInfo, username, secret); connect(internalInfo, &ThingPairingInfo::finished, this, [this, internalInfo, externalInfo, plugin, addNewThing](){ // Internal pairing failed, so fail the exernal one too. if (internalInfo->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "ConfirmPairing failed for" << internalInfo->thingName() << internalInfo->thingClassId(); externalInfo->finish(internalInfo->status(), internalInfo->displayMessage()); return; } // Internal pairing succeeded, set up the thing. if (!addNewThing && !m_configuredThings.contains(internalInfo->thingId())) { qCWarning(dcThingManager) << "The thing to be reconfigured has disappeared!"; externalInfo->finish(Thing::ThingErrorThingNotFound); return; } ThingClass thingClass = m_supportedThings.value(internalInfo->thingClassId()); Thing *thing = nullptr; if (addNewThing) { thing = new Thing(plugin->pluginId(), thingClass, internalInfo->thingId(), this); if (internalInfo->thingName().isEmpty()) { thing->setName(thingClass.displayName()); } else { thing->setName(internalInfo->thingName()); } } else { thing = m_configuredThings.value(internalInfo->thingId()); thing->setSetupStatus(Thing::ThingSetupStatusInProgress, Thing::ThingErrorNoError); qCDebug(dcThingManager()) << "Reconfiguring thing" << thing; } thing->setParams(internalInfo->params()); ParamList settings = buildParams(thingClass.settingsTypes(), ParamList()); thing->setSettings(settings); ThingSetupInfo *info = setupThing(thing); connect(info, &ThingSetupInfo::finished, thing, [this, info, externalInfo, addNewThing](){ externalInfo->finish(info->status(), info->displayMessage()); if (info->status() != Thing::ThingErrorNoError) { if (addNewThing) { qCWarning(dcThingManager()) << "Failed to set up thing" << info->thing()->name() << "Not adding thing to the system. Error:" << info->status() << info->displayMessage(); info->thing()->deleteLater(); } else { qCWarning(dcThingManager()) << "Failed to reconfigure thing" << info->thing()->name() << "Error:" << info->status() << info->displayMessage(); info->thing()->setSetupStatus(Thing::ThingSetupStatusFailed, info->status(), info->displayMessage()); // TODO: restore parameters? } return; } qCDebug(dcThingManager()) << "Setup complete for thing" << info->thing(); info->thing()->setSetupStatus(Thing::ThingSetupStatusComplete, Thing::ThingErrorNoError); if (addNewThing) { qCDebug(dcThingManager()) << "Thing added:" << info->thing(); m_configuredThings.insert(info->thing()->id(), info->thing()); emit thingAdded(info->thing()); } else { emit thingChanged(info->thing()); } storeConfiguredThings(); postSetupThing(info->thing()); }); }); return externalInfo; } ThingSetupInfo* ThingManagerImplementation::addConfiguredThingInternal(const ThingClassId &thingClassId, const QString &name, const ParamList ¶ms, const ThingId &parentId) { ThingClass thingClass = findThingClass(thingClassId); if (thingClass.id().isNull()) { qCWarning(dcThingManager()) << "Cannot add thing. ThingClass" << thingClassId << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorThingClassNotFound); return info; } if (thingClass.setupMethod() != ThingClass::SetupMethodJustAdd) { qCWarning(dcThingManager()) << "Cannot add thing. This thing cannot be added this way. (SetupMethodJustAdd)"; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorCreationMethodNotSupported); return info; } ThingId thingId = ThingId::createThingId(); // Chances are like 0, but... while (m_configuredThings.contains(thingId)) { thingId = ThingId::createThingId(); } IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot add thing. Plugin for thing class" << thingClass.name() << "not found."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(Thing::ThingErrorPluginNotFound); return info; } // set params ParamList effectiveParams = buildParams(thingClass.paramTypes(), params); Thing::ThingError paramsResult = ThingUtils::verifyParams(thingClass.paramTypes(), effectiveParams); if (paramsResult != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Cannot add thing. Parameter verification failed."; ThingSetupInfo *info = new ThingSetupInfo(nullptr, this); info->finish(paramsResult); return info; } Thing *thing = new Thing(plugin->pluginId(), thingClass, thingId, this); thing->setParentId(parentId); if (name.isEmpty()) { thing->setName(thingClass.name()); } else { thing->setName(name); } thing->setParams(effectiveParams); // set settings (init with defaults) ParamList settings = buildParams(thingClass.settingsTypes(), ParamList()); qCDebug(dcThingManager()) << "Adding thing settings" << settings << thingId; thing->setSettings(settings); ThingSetupInfo *info = setupThing(thing); connect(info, &ThingSetupInfo::finished, this, [this, info](){ if (info->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager) << "Thing setup failed. Not adding thing to system."; info->thing()->deleteLater(); return; } info->thing()->setSetupStatus(Thing::ThingSetupStatusComplete, Thing::ThingErrorNoError); qCDebug(dcThingManager) << "Thing setup complete."; m_configuredThings.insert(info->thing()->id(), info->thing()); storeConfiguredThings(); postSetupThing(info->thing()); emit thingAdded(info->thing()); }); return info; } Thing::ThingError ThingManagerImplementation::removeConfiguredThing(const ThingId &thingId) { Thing *thing = m_configuredThings.take(thingId); if (!thing) { return Thing::ThingErrorThingNotFound; } IntegrationPlugin *plugin = m_integrationPlugins.value(thing->pluginId()); if (!plugin) { qCWarning(dcThingManager()).nospace() << "Plugin not loaded for thing " << thing->name() << ". Not calling thingRemoved on plugin."; } else { plugin->thingRemoved(thing); } thing->deleteLater(); NymeaSettings settings(NymeaSettings::SettingsRoleThings); settings.beginGroup("ThingConfig"); settings.beginGroup(thingId.toString()); settings.remove(""); settings.endGroup(); NymeaSettings stateCache(NymeaSettings::SettingsRoleThingStates); stateCache.remove(thingId.toString()); foreach (const IOConnectionId &ioConnectionId, m_ioConnections.keys()) { IOConnection ioConnection = m_ioConnections.value(ioConnectionId); if (ioConnection.inputThingId() == thing->id() || ioConnection.outputThingId() == thing->id()) { disconnectIO(ioConnectionId); } } emit thingRemoved(thingId); return Thing::ThingErrorNoError; } BrowseResult *ThingManagerImplementation::browseThing(const ThingId &thingId, const QString &itemId, const QLocale &locale) { Thing *thing = m_configuredThings.value(thingId); BrowseResult *result = new BrowseResult(thing, this, itemId, locale, this, 30000); if (!thing) { qCWarning(dcThingManager()) << "Cannot browse thing. No such thing:" << thingId.toString(); result->finish(Thing::ThingErrorThingNotFound); return result; } IntegrationPlugin *plugin = m_integrationPlugins.value(thing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot browse thing. Plugin not found for thing" << thing; return result; } if (!thing->setupComplete()) { qCWarning(dcThingManager()) << "Cannot browse thing. Thing did not finish setup" << thing; return result; } if (!thing->thingClass().browsable()) { qCWarning(dcThingManager()) << "Cannot browse thing. ThingClass" << thing->thingClass().name() << "is not browsable."; result->finish(Thing::ThingErrorUnsupportedFeature); return result; } plugin->browseThing(result); connect(result, &BrowseResult::finished, this, [result](){ if (result->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Browse thing failed:" << result->status(); } }); return result; } BrowserItemResult *ThingManagerImplementation::browserItemDetails(const ThingId &thingId, const QString &itemId, const QLocale &locale) { Thing *thing = m_configuredThings.value(thingId); BrowserItemResult *result = new BrowserItemResult(thing, this, itemId, locale, this, 30000); if (!thing) { qCWarning(dcThingManager()) << "Cannot browse thing. No such thing:" << thingId.toString(); result->finish(Thing::ThingErrorThingNotFound); return result; } IntegrationPlugin *plugin = m_integrationPlugins.value(thing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot browse thing. Plugin not found for thing" << thing; return result; } if (thing->setupStatus() != Thing::ThingSetupStatusComplete) { qCWarning(dcThingManager()) << "Cannot browse thing. Thing did not finish setup" << thing; return result; } if (!thing->thingClass().browsable()) { qCWarning(dcThingManager()) << "Cannot browse thing. ThingClass" << thing->thingClass().name() << "is not browsable."; result->finish(Thing::ThingErrorUnsupportedFeature); return result; } plugin->browserItem(result); connect(result, &BrowserItemResult::finished, this, [result](){ if (result->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Browsing thing failed:" << result->status(); } }); return result; } BrowserActionInfo* ThingManagerImplementation::executeBrowserItem(const BrowserAction &browserAction) { Thing *thing = m_configuredThings.value(browserAction.thingId()); BrowserActionInfo *info = new BrowserActionInfo(thing, this, browserAction, this, 30000); if (!thing) { info->finish(Thing::ThingErrorThingNotFound); return info; } IntegrationPlugin *plugin = m_integrationPlugins.value(thing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot browse thing. Plugin not found for thing" << thing; info->finish(Thing::ThingErrorPluginNotFound); return info; } if (thing->setupStatus() != Thing::ThingSetupStatusComplete) { qCWarning(dcThingManager()) << "Cannot browse thing. Thing did not finish setup" << thing; info->finish(Thing::ThingErrorSetupFailed); return info; } if (!thing->thingClass().browsable()) { info->finish(Thing::ThingErrorUnsupportedFeature); return info; } plugin->executeBrowserItem(info); return info; } BrowserItemActionInfo* ThingManagerImplementation::executeBrowserItemAction(const BrowserItemAction &browserItemAction) { Thing *thing = m_configuredThings.value(browserItemAction.thingId()); BrowserItemActionInfo *info = new BrowserItemActionInfo(thing, this, browserItemAction, this, 30000); if (!thing) { info->finish(Thing::ThingErrorThingNotFound); return info; } IntegrationPlugin *plugin = m_integrationPlugins.value(thing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot execute browser item action. Plugin not found for thing" << thing; info->finish(Thing::ThingErrorPluginNotFound); return info; } if (thing->setupStatus() != Thing::ThingSetupStatusComplete) { qCWarning(dcThingManager()) << "Cannot execute browser item action. Thing did not finish setup" << thing; info->finish(Thing::ThingErrorSetupFailed); return info; } if (!thing->thingClass().browsable()) { info->finish(Thing::ThingErrorUnsupportedFeature); return info; } // TODO: check browserItemAction.params with ThingClass plugin->executeBrowserItemAction(info); return info; } IOConnections ThingManagerImplementation::ioConnections(const ThingId &thingId) const { if (thingId.isNull()) { return m_ioConnections.values(); } IOConnections ioConnections; foreach (const IOConnection &ioConnection, m_ioConnections) { if (ioConnection.inputThingId() == thingId || ioConnection.outputThingId() == thingId) { ioConnections.append(ioConnection); } } return ioConnections; } IOConnectionResult ThingManagerImplementation::connectIO(const IOConnection &connection) { IOConnectionResult result; // Do some sanity checks Thing *inputThing = m_configuredThings.value(connection.inputThingId()); if (!inputThing) { qCWarning(dcThingManager()) << "Could not find inputThing" << connection.inputThingId() << "in configured things. Not adding IO connection."; result.error = Thing::ThingErrorThingNotFound; return result; } if (!inputThing->thingClass().stateTypes().contains(connection.inputStateTypeId())) { qCWarning(dcThingManager()) << "Input thing" << inputThing->name() << "does not have a state with id" << connection.inputStateTypeId(); result.error = Thing::ThingErrorStateTypeNotFound; return result; } StateType inputStateType = inputThing->thingClass().stateTypes().findById(connection.inputStateTypeId()); // Check if this is actually an input if (inputStateType.ioType() != Types::IOTypeDigitalInput && inputStateType.ioType() != Types::IOTypeAnalogInput) { qCWarning(dcThingManager()) << "The given input state is neither a digital nor an analog input."; result.error = Thing::ThingErrorInvalidParameter; return result; } Thing *outputThing = m_configuredThings.value(connection.outputThingId()); if (!outputThing) { qCWarning(dcThingManager()) << "Could not find outputThing" << connection.outputThingId() << "in configured things. Not adding IO connection."; result.error = Thing::ThingErrorThingNotFound; return result; } if (!outputThing->thingClass().stateTypes().contains(connection.outputStateTypeId())) { qCWarning(dcThingManager()) << "Output thing" << outputThing->name() << "does not have a state with id" << connection.outputStateTypeId(); result.error = Thing::ThingErrorStateTypeNotFound; return result; } StateType outputStateType = outputThing->thingClass().stateTypes().findById(connection.outputStateTypeId()); // Check if this is actually an output if (outputStateType.ioType() != Types::IOTypeDigitalOutput && outputStateType.ioType() != Types::IOTypeAnalogOutput) { qCWarning(dcThingManager()) << "The given output state is neither a digital nor an analog output."; result.error = Thing::ThingErrorInvalidParameter; return result; } // Check if io types are compatible if (inputStateType.ioType() == Types::IOTypeDigitalInput && outputStateType.ioType() != Types::IOTypeDigitalOutput) { qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType(); result.error = Thing::ThingErrorInvalidParameter; return result; } if (inputStateType.ioType() == Types::IOTypeAnalogInput && outputStateType.ioType() != Types::IOTypeAnalogOutput) { qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType(); result.error = Thing::ThingErrorInvalidParameter; return result; } // Check if either input or output is already connected foreach (const IOConnectionId &id, m_ioConnections.keys()) { if (m_ioConnections.value(id).inputThingId() == connection.inputThingId() && m_ioConnections.value(id).inputStateTypeId() == connection.inputStateTypeId()) { qCDebug(dcThingManager()).nospace() << "Thing " << inputThing->name() << " already has an IO connection on " << inputStateType.displayName() << ". Replacing old connection."; disconnectIO(id); continue; } if (m_ioConnections.value(id).outputThingId() == connection.outputThingId() && m_ioConnections.value(id).outputStateTypeId() == connection.outputStateTypeId()) { qCDebug(dcThingManager()).nospace() << "Thing " << inputThing->name() << " already has an IO connection on " << inputStateType.displayName() << ". Replacing old connection."; disconnectIO(id); } } // Finally add the connection m_ioConnections.insert(connection.id(), connection); storeIOConnections(); emit ioConnectionAdded(connection); qCDebug(dcThingManager()) << "IO connected added:" << inputThing << "->" << outputThing; // Sync initial state syncIOConnection(inputThing, connection.inputStateTypeId()); result.error = Thing::ThingErrorNoError; result.ioConnectionId = connection.id(); return result; } Thing::ThingError ThingManagerImplementation::disconnectIO(const IOConnectionId &ioConnectionId) { if (!m_ioConnections.contains(ioConnectionId)) { qCWarning(dcThingManager()) << "IO connection" << ioConnectionId << "not found. Cannot disconnect."; return Thing::ThingErrorItemNotFound; } m_ioConnections.remove(ioConnectionId); NymeaSettings settings(NymeaSettings::SettingsRoleIOConnections); settings.beginGroup("IOConnections"); settings.remove(ioConnectionId.toString()); settings.endGroup(); qCDebug(dcThingManager()) << "IO connection disconnected:" << ioConnectionId; emit ioConnectionRemoved(ioConnectionId); return Thing::ThingErrorNoError; } QString ThingManagerImplementation::translate(const PluginId &pluginId, const QString &string, const QLocale &locale) { return m_translator->translate(pluginId, string, locale); } ParamType ThingManagerImplementation::translateParamType(const PluginId &pluginId, const ParamType ¶mType, const QLocale &locale) { ParamType translatedParamType = paramType; translatedParamType.setDisplayName(translate(pluginId, paramType.displayName(), locale)); return translatedParamType; } StateType ThingManagerImplementation::translateStateType(const PluginId &pluginId, const StateType &stateType, const QLocale &locale) { StateType translatedStateType = stateType; translatedStateType.setDisplayName(translate(pluginId, stateType.displayName(), locale)); return translatedStateType; } EventType ThingManagerImplementation::translateEventType(const PluginId &pluginId, const EventType &eventType, const QLocale &locale) { EventType translatedEventType = eventType; translatedEventType.setDisplayName(translate(pluginId, eventType.displayName(), locale)); return translatedEventType; } ActionType ThingManagerImplementation::translateActionType(const PluginId &pluginId, const ActionType &actionType, const QLocale &locale) { ActionType translatedActionType = actionType; translatedActionType.setDisplayName(translate(pluginId, actionType.displayName(), locale)); return translatedActionType; } ThingClass ThingManagerImplementation::translateThingClass(const ThingClass &thingClass, const QLocale &locale) { ThingClass translatedThingClass = thingClass; translatedThingClass.setDisplayName(translate(thingClass.pluginId(), thingClass.displayName(), locale)); ParamTypes translatedSettingsTypes; foreach (const ParamType ¶mType, thingClass.settingsTypes()) { translatedSettingsTypes.append(translateParamType(thingClass.pluginId(), paramType, locale)); } translatedThingClass.setSettingsTypes(translatedSettingsTypes); ParamTypes translatedParamTypes; foreach (const ParamType ¶mType, thingClass.paramTypes()) { translatedParamTypes.append(translateParamType(thingClass.pluginId(), paramType, locale)); } translatedThingClass.setParamTypes(translatedParamTypes); StateTypes translatedStateTypes; foreach (const StateType &stateType, thingClass.stateTypes()) { translatedStateTypes.append(translateStateType(thingClass.pluginId(), stateType, locale)); } translatedThingClass.setStateTypes(translatedStateTypes); EventTypes translatedEventTypes; foreach (const EventType &eventType, thingClass.eventTypes()) { translatedEventTypes.append(translateEventType(thingClass.pluginId(), eventType, locale)); } translatedThingClass.setEventTypes(translatedEventTypes); ActionTypes translatedActionTypes; foreach (const ActionType &actionType, thingClass.actionTypes()) { translatedActionTypes.append(translateActionType(thingClass.pluginId(), actionType, locale)); } translatedThingClass.setActionTypes(translatedActionTypes); return translatedThingClass; } Vendor ThingManagerImplementation::translateVendor(const Vendor &vendor, const QLocale &locale) { IntegrationPlugin *plugin = nullptr; foreach (IntegrationPlugin *p, m_integrationPlugins) { if (p->supportedVendors().contains(vendor)) { plugin = p; } } if (!plugin) { return vendor; } Vendor translatedVendor = vendor; translatedVendor.setDisplayName(translate(plugin->pluginId(), vendor.displayName(), locale)); return translatedVendor; } Thing *ThingManagerImplementation::findConfiguredThing(const ThingId &id) const { foreach (Thing *thing, m_configuredThings) { if (thing->id() == id) { return thing; } } return nullptr; } Things ThingManagerImplementation::configuredThings() const { return m_configuredThings.values(); } Things ThingManagerImplementation::findConfiguredThings(const ThingClassId &thingClassId) const { QList ret; foreach (Thing *thing, m_configuredThings) { if (thing->thingClassId() == thingClassId) { ret << thing; } } return ret; } Things ThingManagerImplementation::findConfiguredThings(const QString &interface) const { QList ret; foreach (Thing *thing, m_configuredThings) { ThingClass thingClass = m_supportedThings.value(thing->thingClassId()); if (thingClass.interfaces().contains(interface)) { ret.append(thing); } } return ret; } Things ThingManagerImplementation::findChilds(const ThingId &id) const { QList ret; foreach (Thing *d, m_configuredThings) { if (d->parentId() == id) { ret.append(d); } } return ret; } ThingClass ThingManagerImplementation::findThingClass(const ThingClassId &thingClassId) const { foreach (const ThingClass &thingClass, m_supportedThings) { if (thingClass.id() == thingClassId) { return thingClass; } } return ThingClass(); } ThingActionInfo *ThingManagerImplementation::executeAction(const Action &action) { Action finalAction = action; Thing *thing = m_configuredThings.value(action.thingId()); if (!thing) { qCWarning(dcThingManager()) << "Cannot execute action. No such thing:" << action.thingId(); ThingActionInfo *info = new ThingActionInfo(nullptr, action, this); info->finish(Thing::ThingErrorThingNotFound); return info; } if (!thing->setupComplete()) { qCWarning(dcThingManager()) << "Cannot execute action. Thing" << thing->name() << "hasn't completed setup."; ThingActionInfo *info = new ThingActionInfo(nullptr, action, this); info->finish(Thing::ThingErrorSetupFailed); return info; } // Make sure this thing has an action type with this id ThingClass thingClass = findThingClass(thing->thingClassId()); ActionType actionType = thingClass.actionTypes().findById(action.actionTypeId()); if (actionType.id().isNull()) { qCWarning(dcThingManager()) << "Cannot execute action. No such action type" << action.actionTypeId(); ThingActionInfo *info = new ThingActionInfo(thing, action, this); info->finish(Thing::ThingErrorActionTypeNotFound); return info; } ParamList finalParams = buildParams(actionType.paramTypes(), action.params()); Thing::ThingError paramCheck = ThingUtils::verifyParams(actionType.paramTypes(), finalParams); if (paramCheck != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Cannot execute action. Parameter verification failed."; ThingActionInfo *info = new ThingActionInfo(thing, action, this); info->finish(paramCheck); return info; } finalAction.setParams(finalParams); ThingActionInfo *info = new ThingActionInfo(thing, finalAction, this, 30000); IntegrationPlugin *plugin = m_integrationPlugins.value(thing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Cannot execute action. Plugin not found for device" << thing->name(); info->finish(Thing::ThingErrorPluginNotFound); return info; } plugin->executeAction(info); return info; } void ThingManagerImplementation::loadPlugins() { foreach (const QString &path, pluginSearchDirs()) { QDir dir(path); qCDebug(dcThingManager) << "Loading plugins from:" << dir.absolutePath(); foreach (const QString &entry, dir.entryList()) { QFileInfo fi; if (entry.startsWith("libnymea_integrationplugin") && entry.endsWith(".so")) { fi.setFile(path + "/" + entry); } else { fi.setFile(path + "/" + entry + "/libnymea_integrationplugin" + entry + ".so"); } if (!fi.exists()) continue; // Check plugin API version compatibility QLibrary lib(fi.absoluteFilePath()); if (!lib.load()) { qCWarning(dcThingManager()).nospace() << "Error loading plugin " << fi.absoluteFilePath() << ": " << lib.errorString(); continue; } QFunctionPointer versionFunc = lib.resolve("libnymea_api_version"); if (!versionFunc) { qCWarning(dcThingManager()).nospace() << "Unable to resolve version in plugin " << fi.absoluteFilePath() << ". Not loading plugin."; lib.unload(); continue; } QString version = reinterpret_cast(versionFunc)(); lib.unload(); QStringList parts = version.split('.'); QStringList coreParts = QString(LIBNYMEA_API_VERSION).split('.'); if (parts.length() != 3 || parts.at(0).toInt() != coreParts.at(0).toInt() || parts.at(1).toInt() > coreParts.at(1).toInt()) { qCWarning(dcThingManager()).nospace() << "Libnymea API mismatch for " << fi.absoluteFilePath() << ". Core API: " << LIBNYMEA_API_VERSION << ", Plugin API: " << version; continue; } // Version is ok. Now load the plugin QPluginLoader loader; loader.setFileName(fi.absoluteFilePath()); loader.setLoadHints(QLibrary::ResolveAllSymbolsHint); qCDebug(dcThingManager()) << "Loading plugin from:" << fi.absoluteFilePath(); if (!loader.load()) { qCWarning(dcThingManager) << "Could not load plugin data of" << entry << "\n" << loader.errorString(); continue; } QJsonObject pluginInfo = loader.metaData().value("MetaData").toObject(); PluginMetadata metaData(pluginInfo, false, false); if (!metaData.isValid()) { foreach (const QString &error, metaData.validationErrors()) { qCWarning(dcThingManager()) << error; } loader.unload(); continue; } IntegrationPlugin *pluginIface = qobject_cast(loader.instance()); if (!pluginIface) { qCWarning(dcThingManager) << "Could not get plugin instance of" << entry; loader.unload(); continue; } if (m_integrationPlugins.contains(pluginIface->pluginId())) { qCWarning(dcThingManager()) << "A plugin with this ID is already loaded. Not loading" << entry; continue; } loadPlugin(pluginIface, metaData); PluginInfoCache::cachePluginInfo(pluginInfo); } } #if QT_VERSION >= QT_VERSION_CHECK(5,12,0) foreach (const QString &path, pluginSearchDirs()) { QDir dir(path); qCDebug(dcThingManager) << "Loading JS plugins from:" << dir.absolutePath(); foreach (const QString &entry, dir.entryList()) { QFileInfo jsFi; QFileInfo jsonFi; if (entry.endsWith(".js")) { jsFi.setFile(path + "/" + entry); } else { jsFi.setFile(path + "/" + entry + "/" + entry + ".js"); } if (!jsFi.exists()) { continue; } ScriptIntegrationPlugin *plugin = new ScriptIntegrationPlugin(this); bool ret = plugin->loadScript(jsFi.absoluteFilePath()); if (!ret) { delete plugin; qCWarning(dcThingManager()) << "JS plugin failed to load"; continue; } PluginMetadata metaData(plugin->metaData()); if (!metaData.isValid()) { qCWarning(dcThingManager()) << "Not loading JS plugin. Invalid metadata."; foreach (const QString &error, metaData.validationErrors()) { qCWarning(dcThingManager()) << error; } } loadPlugin(plugin, metaData); } } #endif } void ThingManagerImplementation::loadPlugin(IntegrationPlugin *pluginIface, const PluginMetadata &metaData) { pluginIface->setParent(this); pluginIface->initPlugin(metaData, this, m_hardwareManager); qCDebug(dcThingManager) << "**** Loaded plugin" << pluginIface->pluginName(); foreach (const Vendor &vendor, pluginIface->supportedVendors()) { qCDebug(dcThingManager) << "* Loaded vendor:" << vendor.name() << vendor.id(); if (m_supportedVendors.contains(vendor.id())) continue; m_supportedVendors.insert(vendor.id(), vendor); } foreach (const ThingClass &thingClass, pluginIface->supportedThings()) { if (!m_supportedVendors.contains(thingClass.vendorId())) { qCWarning(dcThingManager) << "Vendor not found. Ignoring thing. VendorId:" << thingClass.vendorId() << "ThingClass:" << thingClass.name() << thingClass.id(); continue; } m_vendorThingMap[thingClass.vendorId()].append(thingClass.id()); m_supportedThings.insert(thingClass.id(), thingClass); qCDebug(dcThingManager) << "* Loaded thing class:" << thingClass.name(); } NymeaSettings settings(NymeaSettings::SettingsRolePlugins); settings.beginGroup("PluginConfig"); ParamList params; if (settings.childGroups().contains(pluginIface->pluginId().toString())) { settings.beginGroup(pluginIface->pluginId().toString()); if (!settings.childGroups().isEmpty()) { // Note: since nymea 0.12.2 the param type gets saved too for better data converting foreach (const QString ¶mTypeIdString, settings.childGroups()) { ParamTypeId paramTypeId(paramTypeIdString); ParamType paramType = pluginIface->configurationDescription().findById(paramTypeId); if (!paramType.isValid()) { qCWarning(dcThingManager()) << "Not loading Param for plugin" << pluginIface->pluginName() << "because the ParamType for the saved Param" << ParamTypeId(paramTypeIdString).toString() << "could not be found."; continue; } QVariant paramValue; settings.beginGroup(paramTypeIdString); paramValue = settings.value("value", paramType.defaultValue()); paramValue.convert(settings.value("type").toInt()); params.append(Param(paramTypeId, paramValue)); settings.endGroup(); } } else { // Note: < nymea 0.12.2 foreach (const QString ¶mTypeIdString, settings.allKeys()) { params.append(Param(ParamTypeId(paramTypeIdString), settings.value(paramTypeIdString))); } } settings.endGroup(); } else if (!pluginIface->configurationDescription().isEmpty()){ // plugin requires config but none stored. Init with defaults foreach (const ParamType ¶mType, pluginIface->configurationDescription()) { Param param(paramType.id(), paramType.defaultValue()); params.append(param); } } settings.endGroup(); if (params.count() > 0) { Thing::ThingError status = pluginIface->setConfiguration(params); if (status != Thing::ThingErrorNoError) { qCWarning(dcThingManager) << "Error setting params to plugin. Broken configuration?"; } } // Call the init method of the plugin pluginIface->init(); m_integrationPlugins.insert(pluginIface->pluginId(), pluginIface); connect(pluginIface, &IntegrationPlugin::emitEvent, this, &ThingManagerImplementation::onEventTriggered, Qt::QueuedConnection); connect(pluginIface, &IntegrationPlugin::autoThingsAppeared, this, &ThingManagerImplementation::onAutoThingsAppeared, Qt::QueuedConnection); connect(pluginIface, &IntegrationPlugin::autoThingDisappeared, this, &ThingManagerImplementation::onAutoThingDisappeared, Qt::QueuedConnection); } void ThingManagerImplementation::loadConfiguredThings() { bool needsMigration = false; NymeaSettings settings(NymeaSettings::SettingsRoleThings); if (settings.childGroups().contains("ThingConfig")) { settings.beginGroup("ThingConfig"); } else { settings.beginGroup("DeviceConfig"); needsMigration = true; } qCDebug(dcThingManager) << "Loading things from" << settings.fileName(); foreach (const QString &idString, settings.childGroups()) { settings.beginGroup(idString); QString thingName = settings.value("thingName").toString(); if (!settings.contains("thingName")) { // nymea < 0.20 thingName = settings.value("devicename").toString(); } PluginId pluginId = PluginId(settings.value("pluginid").toString()); IntegrationPlugin *plugin = m_integrationPlugins.value(pluginId); if (!plugin) { qCWarning(dcThingManager()) << "Plugin for thing" << thingName << idString << "not found. This thing will not be functional until the plugin can be loaded."; } ThingClassId thingClassId = ThingClassId(settings.value("thingClassId").toString()); // If ThingClassId isn't found in the config, retry with ThingClassId (nymea < 0.20) if (thingClassId.isNull()) { thingClassId = ThingClassId(settings.value("deviceClassId").toString()); } ThingClass thingClass = findThingClass(thingClassId); if (!thingClass.isValid()) { // Try to load the device class from the cache QJsonObject pluginInfo = PluginInfoCache::loadPluginInfo(pluginId); if (!pluginInfo.empty()) { PluginMetadata pluginMetadata(pluginInfo, false, false); thingClass = pluginMetadata.thingClasses().findById(thingClassId); if (thingClass.isValid()) { m_supportedThings.insert(thingClassId, thingClass); if (!m_supportedVendors.contains(thingClass.vendorId())) { Vendor vendor = pluginMetadata.vendors().findById(thingClass.vendorId()); m_supportedVendors.insert(vendor.id(), vendor); } } } } if (!thingClass.isValid()) { qCWarning(dcThingManager()) << "Not loading thing" << thingName << idString << "because the thing class for this thing could not be found."; settings.endGroup(); // ThingId continue; } // Cross-check if this plugin still implements this thing class if (plugin && !plugin->supportedThings().contains(thingClass)) { qCWarning(dcThingManager()) << "Not loading thing" << thingName << idString << "because plugin" << plugin->pluginName() << "has removed support for it."; settings.endGroup(); // ThingId continue; } Thing *thing = new Thing(pluginId, thingClass, ThingId(idString), this); thing->m_autoCreated = settings.value("autoCreated").toBool(); thing->setName(thingName); thing->setParentId(ThingId(settings.value("parentid", QUuid()).toString())); ParamList params; settings.beginGroup("Params"); if (!settings.childGroups().isEmpty()) { foreach (const QString ¶mTypeIdString, settings.childGroups()) { ParamTypeId paramTypeId(paramTypeIdString); ParamType paramType = thingClass.paramTypes().findById(paramTypeId); QVariant defaultValue; if (!paramType.isValid()) { // NOTE: We're not skipping unknown parameters to give plugins a chance to still access old values if they change their config and migrate things over. qCWarning(dcThingManager()) << "Unknown param" << paramTypeIdString << "for" << thing << ". ParamType could not be found in device class."; } // Note: since nymea 0.12.2 QVariant paramValue; settings.beginGroup(paramTypeIdString); paramValue = settings.value("value", paramType.defaultValue()); paramValue.convert(settings.value("type").toInt()); params.append(Param(paramTypeId, paramValue)); settings.endGroup(); // ParamId } } else { foreach (const QString ¶mTypeIdString, settings.allKeys()) { params.append(Param(ParamTypeId(paramTypeIdString), settings.value(paramTypeIdString))); } } // Make sure all params are around. if they aren't initialize with default values foreach (const ParamType ¶mType, thingClass.paramTypes()) { if (!params.hasParam(paramType.id())) { params.append(Param(paramType.id(), paramType.defaultValue())); } } thing->setParams(params); settings.endGroup(); // Params ParamList thingSettings; settings.beginGroup("Settings"); if (!settings.childGroups().isEmpty()) { foreach (const QString ¶mTypeIdString, settings.childGroups()) { ParamTypeId paramTypeId(paramTypeIdString); ParamType paramType = thingClass.settingsTypes().findById(paramTypeId); if (!paramType.isValid()) { qCWarning(dcThingManager()) << "Not loading Setting for thing" << thing << "because the ParamType for the saved Setting" << ParamTypeId(paramTypeIdString).toString() << "could not be found."; continue; } // Note: since nymea 0.12.2 QVariant paramValue; settings.beginGroup(paramTypeIdString); paramValue = settings.value("value", paramType.defaultValue()); paramValue.convert(settings.value("type").toInt()); thingSettings.append(Param(paramTypeId, paramValue)); settings.endGroup(); // ParamId } } else { foreach (const QString ¶mTypeIdString, settings.allKeys()) { params.append(Param(ParamTypeId(paramTypeIdString), settings.value(paramTypeIdString))); } } // Fill in any missing params with defaults thingSettings = buildParams(thingClass.settingsTypes(), thingSettings); thing->setSettings(thingSettings); settings.endGroup(); // Settings settings.endGroup(); // ThingId // We always add the thing to the list in this case. If it's in the stored things // it means that it was working at some point so lets still add it as there might // be rules associated with this thing. m_configuredThings.insert(thing->id(), thing); emit thingAdded(thing); } settings.endGroup(); if (needsMigration) { storeConfiguredThings(); settings.remove("DeviceConfig"); } QHash setupList = m_configuredThings; while (!setupList.isEmpty()) { Thing *thing = nullptr; foreach (Thing *d, setupList) { if (d->parentId().isNull() || !setupList.contains(d->parentId())) { thing = d; setupList.take(d->id()); break; } } Q_ASSERT(thing != nullptr); thing->setSetupStatus(Thing::ThingSetupStatusInProgress, Thing::ThingErrorNoError); ThingSetupInfo *info = setupThing(thing); // Set receiving object to "thing" because at startup we load it in any case, knowing that it worked at // some point. However, it'll be marked as non-working until the setup succeeds so the user might delete // it in the meantime... In that case we don't want to call postsetup on it. connect(info, &ThingSetupInfo::finished, thing, [this, info](){ if (info->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "Error setting up thing" << info->thing()->name() << info->thing()->id().toString() << info->status() << info->displayMessage(); info->thing()->setSetupStatus(Thing::ThingSetupStatusFailed, info->status(), info->displayMessage()); emit thingChanged(info->thing()); return; } qCDebug(dcThingManager()) << "Setup complete for thing" << info->thing(); info->thing()->setSetupStatus(Thing::ThingSetupStatusComplete, Thing::ThingErrorNoError); emit thingChanged(info->thing()); postSetupThing(info->thing()); }); } loadIOConnections(); } void ThingManagerImplementation::storeConfiguredThings() { NymeaSettings settings(NymeaSettings::SettingsRoleThings); settings.beginGroup("ThingConfig"); foreach (Thing *thing, m_configuredThings) { settings.beginGroup(thing->id().toString()); // Note: clean thing settings before storing it for clean up settings.remove(""); settings.setValue("autoCreated", thing->autoCreated()); settings.setValue("thingName", thing->name()); settings.setValue("thingClassId", thing->thingClassId().toString()); settings.setValue("pluginid", thing->pluginId().toString()); if (!thing->parentId().isNull()) settings.setValue("parentid", thing->parentId().toString()); settings.beginGroup("Params"); foreach (const Param ¶m, thing->params()) { settings.beginGroup(param.paramTypeId().toString()); settings.setValue("type", static_cast(param.value().type())); settings.setValue("value", param.value()); settings.endGroup(); // ParamTypeId } settings.endGroup(); // Params settings.beginGroup("Settings"); foreach (const Param ¶m, thing->settings()) { settings.beginGroup(param.paramTypeId().toString()); settings.setValue("type", static_cast(param.value().type())); settings.setValue("value", param.value()); settings.endGroup(); // ParamTypeId } settings.endGroup(); // Settings settings.endGroup(); // ThingId } settings.endGroup(); // ThingConfig } void ThingManagerImplementation::startMonitoringAutoThings() { foreach (IntegrationPlugin *plugin, m_integrationPlugins) { plugin->startMonitoringAutoThings(); } } void ThingManagerImplementation::onAutoThingsAppeared(const ThingDescriptors &thingDescriptors) { foreach (const ThingDescriptor &thingDescriptor, thingDescriptors) { ThingClass thingClass = findThingClass(thingDescriptor.thingClassId()); if (!thingClass.isValid()) { qCWarning(dcThingManager()) << "Ignoring appearing auto thing for an unknown ThingClass" << thingDescriptor.thingClassId(); return; } IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); if (!plugin) { return; } if (!thingDescriptor.parentId().isNull() && !m_configuredThings.contains(thingDescriptor.parentId())) { qCWarning(dcThingManager()) << "Invalid parent thing id. Not adding thing to the system."; continue; } Thing *thing = nullptr; // If the appeared auto thing holds a valid thing id, do a reconfiguration for this thing if (!thingDescriptor.thingId().isNull()) { thing = findConfiguredThing(thingDescriptor.thingId()); if (!thing) { qCWarning(dcThingManager()) << "Could not find thing for auto thing descriptor" << thingDescriptor.thingId(); continue; } qCDebug(dcThingManager()) << "Start reconfiguring auto thing" << thing; ParamList finalParams = buildParams(thingClass.paramTypes(), thingDescriptor.params()); reconfigureThingInternal(thing, finalParams); continue; } thing = new Thing(plugin->pluginId(), thingClass, this); thing->m_autoCreated = true; thing->setName(thingDescriptor.title()); thing->setParams(thingDescriptor.params()); ParamList settings = buildParams(thingClass.settingsTypes(), ParamList()); thing->setSettings(settings); thing->setParentId(thingDescriptor.parentId()); qCDebug(dcThingManager()) << "Setting up auto thing:" << thing->name() << thing->id().toString(); ThingSetupInfo *info = setupThing(thing); connect(info, &ThingSetupInfo::finished, thing, [this, info](){ if (info->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager) << "Thing setup failed. Not adding auto thing to system."; info->thing()->deleteLater(); return; } info->thing()->setSetupStatus(Thing::ThingSetupStatusComplete, Thing::ThingErrorNoError); m_configuredThings.insert(info->thing()->id(), info->thing()); storeConfiguredThings(); emit thingAdded(info->thing()); postSetupThing(info->thing()); }); } } void ThingManagerImplementation::onAutoThingDisappeared(const ThingId &thingId) { IntegrationPlugin *plugin = static_cast(sender()); Thing *thing = m_configuredThings.value(thingId); if (!thing) { qWarning(dcThingManager) << "Received an autoThingDisappeared signal but this thing is unknown:" << thingId; return; } ThingClass thingClass = m_supportedThings.value(thing->thingClassId()); if (thingClass.pluginId() != plugin->pluginId()) { qWarning(dcThingManager) << "Received a autoThingDisappeared signal but emitting plugin does not own the thing"; return; } if (!thing->autoCreated()) { qWarning(dcThingManager) << "Received an autoThingDisappeared signal but thing creationMethod is not CreateMothodAuto"; return; } emit thingDisappeared(thingId); } void ThingManagerImplementation::onLoaded() { qCDebug(dcThingManager()) << "Done loading plugins and things."; emit loaded(); // schedule some housekeeping... QTimer::singleShot(0, this, SLOT(cleanupThingStateCache())); } void ThingManagerImplementation::cleanupThingStateCache() { NymeaSettings settings(NymeaSettings::SettingsRoleThingStates); foreach (const QString &entry, settings.childGroups()) { ThingId thingId(entry); if (!m_configuredThings.contains(thingId)) { qCDebug(dcThingManager()) << "Thing ID" << thingId << "not found in configured things. Cleaning up stale thing state cache."; settings.remove(entry); } } } void ThingManagerImplementation::onEventTriggered(const Event &event) { // Doing some sanity checks here... Thing *thing = m_configuredThings.value(event.thingId()); if (!thing) { qCWarning(dcThingManager()) << "Invalid thing id in emitted event. Not forwarding event. Thing setup not complete yet?"; return; } EventType eventType = thing->thingClass().eventTypes().findById(event.eventTypeId()); if (!eventType.isValid()) { qCWarning(dcThingManager()) << "The given thing does not have an event type of id " + event.eventTypeId().toString() + ". Not forwarding event."; return; } // All good, forward the event emit eventTriggered(event); } void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &stateTypeId, const QVariant &value) { Thing *thing = qobject_cast(sender()); if (!thing || !m_configuredThings.contains(thing->id())) { qCWarning(dcThingManager()) << "Invalid thing id in state change. Not forwarding event. Thing setup not complete yet?"; return; } storeThingStates(thing); emit thingStateChanged(thing, stateTypeId, value); Param valueParam(ParamTypeId(stateTypeId.toString()), value); Event event(EventTypeId(stateTypeId.toString()), thing->id(), ParamList() << valueParam, true); emit eventTriggered(event); syncIOConnection(thing, stateTypeId); } void ThingManagerImplementation::syncIOConnection(Thing *thing, const StateTypeId &stateTypeId) { foreach (const IOConnection &ioConnection, m_ioConnections) { // Check if this state is an input to an IO connection. if (ioConnection.inputThingId() == thing->id() && ioConnection.inputStateTypeId() == stateTypeId) { Thing *inputThing = thing; QVariant inputValue = inputThing->stateValue(stateTypeId); Thing *outputThing = m_configuredThings.value(ioConnection.outputThingId()); if (!outputThing) { qCWarning(dcThingManager()) << "IO connection contains invalid output thing!"; continue; } IntegrationPlugin *plugin = m_integrationPlugins.value(outputThing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Plugin not found for IO connection's output action."; continue; } StateType inputStateType = inputThing->thingClass().getStateType(stateTypeId); StateType outputStateType = outputThing->thingClass().getStateType(ioConnection.outputStateTypeId()); if (outputStateType.id().isNull()) { qCWarning(dcThingManager()) << "Could not find output state type for IO connection."; continue; } QVariant outputValue; if (outputStateType.ioType() == Types::IOTypeDigitalOutput) { // Digital IOs are mapped as-is outputValue = ioConnection.inverted() xor inputValue.toBool(); // We're already in sync! Skipping action. if (outputThing->stateValue(outputStateType.id()) == outputValue) { continue; } } else { // Analog IOs are mapped within the according min/max ranges outputValue = mapValue(inputValue, inputStateType, outputStateType, ioConnection.inverted()); // We're already in sync (fuzzy, good enough)! Skipping action. if (qFuzzyCompare(1.0 + outputThing->stateValue(outputStateType.id()).toDouble(), 1.0 + outputValue.toDouble())) { continue; } } Action outputAction(ActionTypeId(ioConnection.outputStateTypeId()), ioConnection.outputThingId()); Param outputParam(ioConnection.outputStateTypeId(), outputValue); outputAction.setParams(ParamList() << outputParam); qCDebug(dcThingManager()) << "Executing IO connection action on" << outputThing->name() << outputParam; ThingActionInfo* info = executeAction(outputAction); connect(info, &ThingActionInfo::finished, this, [=](){ if (info->status() != Thing::ThingErrorNoError) { // An error happened... let's switch the input back to be in sync with the output qCWarning(dcThingManager()) << "Error syncing IO connection state. Reverting input back to old value."; if (inputStateType.ioType() == Types::IOTypeDigitalInput) { inputThing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id())); } else { inputThing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType, ioConnection.inverted())); } } }); } // Now check if this is an output state type and - if possible - update the inputs for bidirectional connections if (ioConnection.outputThingId() == thing->id() && ioConnection.outputStateTypeId() == stateTypeId) { Thing *outputThing = thing; QVariant outputValue = outputThing->stateValue(stateTypeId); Thing *inputThing = m_configuredThings.value(ioConnection.inputThingId()); if (!inputThing) { qCWarning(dcThingManager()) << "IO connection contains invalid input thing!"; continue; } IntegrationPlugin *plugin = m_integrationPlugins.value(inputThing->pluginId()); if (!plugin) { qCWarning(dcThingManager()) << "Plugin not found for IO connection's input action."; continue; } StateType outputStateType = outputThing->thingClass().getStateType(stateTypeId); StateType inputStateType = inputThing->thingClass().getStateType(ioConnection.inputStateTypeId()); if (inputStateType.id().isNull()) { qCWarning(dcThingManager()) << "Could not find input state type for IO connection."; continue; } if (!inputStateType.writable()) { qCDebug(dcThingManager()) << "Input state is not writable. This connection is unidirectional."; continue; } QVariant inputValue; if (inputStateType.ioType() == Types::IOTypeDigitalInput) { // Digital IOs are mapped as-is inputValue = ioConnection.inverted() xor outputValue.toBool(); // Prevent looping if (inputThing->stateValue(inputStateType.id()) == inputValue) { continue; } } else { // Analog IOs are mapped within the according min/max ranges inputValue = mapValue(outputValue, outputStateType, inputStateType, ioConnection.inverted()); // Prevent looping even if the above calculation has rounding errors... Just skip this action if we're close enough already if (qFuzzyCompare(1.0 + inputThing->stateValue(inputStateType.id()).toDouble(), 1.0 + inputValue.toDouble())) { continue; } } Action inputAction(ActionTypeId(ioConnection.inputStateTypeId()), ioConnection.inputThingId()); Param inputParam(ioConnection.inputStateTypeId(), inputValue); inputAction.setParams(ParamList() << inputParam); qCDebug(dcThingManager()) << "Executing reverse IO connection action on" << inputThing->name() << inputParam; executeAction(inputAction); } } } void ThingManagerImplementation::slotThingSettingChanged(const ParamTypeId ¶mTypeId, const QVariant &value) { Thing *thing = qobject_cast(sender()); if (!thing) { return; } storeConfiguredThings(); emit thingSettingChanged(thing->id(), paramTypeId, value); } void ThingManagerImplementation::slotThingNameChanged() { Thing *thing = qobject_cast(sender()); if (!thing) { return; } storeConfiguredThings(); emit thingChanged(thing); } ParamList ThingManagerImplementation::buildParams(const ParamTypes &types, const ParamList &first, const ParamList &second) { // Merge params from discovered descriptor and additional overrides provided on API call. User provided params have higher priority than discovery params. ParamList finalParams; foreach (const ParamType ¶mType, types) { if (first.hasParam(paramType.id())) { finalParams.append(Param(paramType.id(), first.paramValue(paramType.id()))); } else if (second.hasParam(paramType.id())) { finalParams.append(Param(paramType.id(), second.paramValue(paramType.id()))); } else if (paramType.defaultValue().isValid()){ finalParams.append(Param(paramType.id(), paramType.defaultValue())); } } return finalParams; } void ThingManagerImplementation::pairThingInternal(ThingPairingInfo *info) { ThingClass thingClass = m_supportedThings.value(info->thingClassId()); if (thingClass.id().isNull()) { qCWarning(dcThingManager) << "Cannot find a thing class with id" << info->thingClassId(); info->finish(Thing::ThingErrorThingClassNotFound); return; } if (thingClass.setupMethod() == ThingClass::SetupMethodJustAdd) { qCWarning(dcThingManager) << "Cannot setup this thing this way. No need to pair this thing."; info->finish(Thing::ThingErrorSetupMethodNotSupported); return; } IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); if (!plugin) { qCWarning(dcThingManager) << "Cannot pair thing class" << thingClass.name() << "because no plugin for it is loaded."; info->finish(Thing::ThingErrorPluginNotFound); return; } plugin->startPairing(info); connect(info, &ThingPairingInfo::finished, this, [this, info, thingClass](){ if (info->status() != Thing::ThingErrorNoError) { qCWarning(dcThingManager()) << "PairThing failed for" << thingClass << info->status() << info->displayMessage(); return; } qCDebug(dcThingManager()) << "Pairing started for" << thingClass << "using transaction id" << info->transactionId(); PairingContext context; context.thingId = info->thingId(); context.thingClassId = info->thingClassId(); context.thingName = info->thingName(); context.params = info->params(); context.parentId = info->parentId(); m_pendingPairings.insert(info->transactionId(), context); // TODO: Add a timer to clean up stuff if confirmPairing is never called. }); } ThingSetupInfo* ThingManagerImplementation::setupThing(Thing *thing) { ThingClass thingClass = findThingClass(thing->thingClassId()); IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); if (!plugin) { qCWarning(dcThingManager) << "Can't find a plugin for this thing" << thing; ThingSetupInfo *info = new ThingSetupInfo(thing, this); info->finish(Thing::ThingErrorPluginNotFound, tr("The plugin for this thing is not loaded.")); return info; } QList states; foreach (const StateType &stateType, thingClass.stateTypes()) { State state(stateType.id(), thing->id()); states.append(state); } thing->setStates(states); loadThingStates(thing); connect(thing, &Thing::stateValueChanged, this, &ThingManagerImplementation::slotThingStateValueChanged); connect(thing, &Thing::settingChanged, this, &ThingManagerImplementation::slotThingSettingChanged); connect(thing, &Thing::nameChanged, this, &ThingManagerImplementation::slotThingNameChanged); ThingSetupInfo *info = new ThingSetupInfo(thing, this, 30000); plugin->setupThing(info); return info; } void ThingManagerImplementation::postSetupThing(Thing *thing) { ThingClass thingClass = findThingClass(thing->thingClassId()); IntegrationPlugin *plugin = m_integrationPlugins.value(thingClass.pluginId()); plugin->postSetupThing(thing); } void ThingManagerImplementation::loadThingStates(Thing *thing) { NymeaSettings settings(NymeaSettings::SettingsRoleThingStates); settings.beginGroup(thing->id().toString()); ThingClass thingClass = m_supportedThings.value(thing->thingClassId()); foreach (const StateType &stateType, thingClass.stateTypes()) { if (stateType.cached()) { QVariant value; // First try to load new style if (settings.childGroups().contains(stateType.id().toString())) { settings.beginGroup(stateType.id().toString()); value = settings.value("value", stateType.defaultValue()); value.convert(settings.value("type").toInt()); settings.endGroup(); } else { // Try to fall back to the pre 0.9.0 way of storing states value = settings.value(stateType.id().toString(), stateType.defaultValue()); } thing->setStateValue(stateType.id(), value); } else { thing->setStateValue(stateType.id(), stateType.defaultValue()); } } settings.endGroup(); } void ThingManagerImplementation::storeIOConnections() { NymeaSettings connectionSettings(NymeaSettings::SettingsRoleIOConnections); connectionSettings.beginGroup("IOConnections"); foreach (const IOConnection &ioConnection, m_ioConnections) { connectionSettings.beginGroup(ioConnection.id().toString()); connectionSettings.setValue("inputThingId", ioConnection.inputThingId().toString()); connectionSettings.setValue("inputStateTypeId", ioConnection.inputStateTypeId().toString()); connectionSettings.setValue("outputThingId", ioConnection.outputThingId().toString()); connectionSettings.setValue("outputStateTypeId", ioConnection.outputStateTypeId().toString()); connectionSettings.setValue("inverted", ioConnection.inverted()); connectionSettings.endGroup(); } connectionSettings.endGroup(); } void ThingManagerImplementation::loadIOConnections() { NymeaSettings connectionSettings(NymeaSettings::SettingsRoleIOConnections); connectionSettings.beginGroup("IOConnections"); foreach (const QString &idString, connectionSettings.childGroups()) { connectionSettings.beginGroup(idString); IOConnectionId id(idString); ThingId inputThingId = connectionSettings.value("inputThingId").toUuid(); StateTypeId inputStateTypeId = connectionSettings.value("inputStateTypeId").toUuid(); ThingId outputThingId = connectionSettings.value("outputThingId").toUuid(); StateTypeId outputStateTypeId = connectionSettings.value("outputStateTypeId").toUuid(); bool inverted = connectionSettings.value("inverted").toBool(); IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted); m_ioConnections.insert(id, ioConnection); connectionSettings.endGroup(); Thing *inputThing = m_configuredThings.value(inputThingId); if (!inputThing) { continue; } syncIOConnection(inputThing, inputStateTypeId); } connectionSettings.endGroup(); } QVariant ThingManagerImplementation::mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const { double fromMin = fromStateType.minValue().toDouble(); double fromMax = fromStateType.maxValue().toDouble(); double toMin = toStateType.minValue().toDouble(); double toMax = toStateType.maxValue().toDouble(); double fromValue = value.toDouble(); double fromPercent = (fromValue - fromMin) / (fromMax - fromMin); fromPercent = inverted ? 1 - fromPercent : fromPercent; double toValue = toMin + (toMax - toMin) * fromPercent; return toValue; } void ThingManagerImplementation::storeThingStates(Thing *thing) { NymeaSettings settings(NymeaSettings::SettingsRoleThingStates); settings.beginGroup(thing->id().toString()); ThingClass thingClass = m_supportedThings.value(thing->thingClassId()); foreach (const StateType &stateType, thingClass.stateTypes()) { if (stateType.cached()) { settings.beginGroup(stateType.id().toString()); settings.setValue("type", static_cast(thing->stateValue(stateType.id()).type())); settings.setValue("value", thing->stateValue(stateType.id())); settings.endGroup(); } } settings.endGroup(); }