From 45caf668514cd7fcb8e333dc0d508eb9593f9a5e Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 10 Jul 2017 12:12:41 +0200 Subject: [PATCH] initial work on interfaces have some interfaces defined and in use by DeviceManager and the API. this can be used to build first prototypes in apps using the interfaces stuff. Currently the lights interfaces are mostly defined and fully implemented by the Hue plugin. TODO: more interfaces to be defined, make more plugins follow interfaces. TODO: tests for the interface code TODO: docs for the interface code --- libguh/devicemanager.cpp | 1 + libguh/interfaces/colorlight.json | 15 +++ libguh/interfaces/dimmablelight.json | 12 +++ libguh/interfaces/garagegate.json | 20 ++++ libguh/interfaces/interfaces.qrc | 9 ++ libguh/interfaces/light.json | 9 ++ libguh/interfaces/mediacontroller.json | 46 ++++++++ libguh/libguh.pro | 6 ++ libguh/plugin/deviceclass.cpp | 10 ++ libguh/plugin/deviceclass.h | 4 + libguh/plugin/deviceplugin.cpp | 102 ++++++++++++++++++ libguh/plugin/deviceplugin.h | 2 + libguh/types/actiontype.cpp | 27 +++++ libguh/types/actiontype.h | 8 ++ libguh/types/eventtype.cpp | 27 +++++ libguh/types/eventtype.h | 8 ++ libguh/types/statetype.cpp | 28 +++++ libguh/types/statetype.h | 8 ++ plugins/deviceplugins/deviceplugins.pro | 59 +++++----- .../deviceplugins/kodi/devicepluginkodi.json | 1 + .../philipshue/devicepluginphilipshue.json | 1 + server/jsonrpc/jsontypes.cpp | 2 + 22 files changed, 377 insertions(+), 28 deletions(-) create mode 100644 libguh/interfaces/colorlight.json create mode 100644 libguh/interfaces/dimmablelight.json create mode 100644 libguh/interfaces/garagegate.json create mode 100644 libguh/interfaces/interfaces.qrc create mode 100644 libguh/interfaces/light.json create mode 100644 libguh/interfaces/mediacontroller.json diff --git a/libguh/devicemanager.cpp b/libguh/devicemanager.cpp index f818ca55..5fe1deed 100644 --- a/libguh/devicemanager.cpp +++ b/libguh/devicemanager.cpp @@ -195,6 +195,7 @@ #include #include #include +#include /*! Constructs the DeviceManager with the given \a locale and \a parent. There should only be one DeviceManager in the system created by \l{guhserver::GuhCore}. * Use \c guhserver::GuhCore::instance()->deviceManager() instead to access the DeviceManager. */ diff --git a/libguh/interfaces/colorlight.json b/libguh/interfaces/colorlight.json new file mode 100644 index 00000000..4054ad69 --- /dev/null +++ b/libguh/interfaces/colorlight.json @@ -0,0 +1,15 @@ +{ + "extends": "dimmablelight", + "states": [ + { + "name": "color temperature", + "type": "int", + "minValue": "any", + "maxValue": "any" + }, + { + "name": "color", + "type": "QColor" + } + ] +} diff --git a/libguh/interfaces/dimmablelight.json b/libguh/interfaces/dimmablelight.json new file mode 100644 index 00000000..1ff6aee0 --- /dev/null +++ b/libguh/interfaces/dimmablelight.json @@ -0,0 +1,12 @@ +{ + "extends": "light", + "states": [ + { + "name": "brightness", + "type": "int", + "minimumValue": 0, + "maximumValue": 100, + "writable": true + } + ] +} diff --git a/libguh/interfaces/garagegate.json b/libguh/interfaces/garagegate.json new file mode 100644 index 00000000..6bfb02d0 --- /dev/null +++ b/libguh/interfaces/garagegate.json @@ -0,0 +1,20 @@ +{ + "states": [ + { + "name": "gateState", + "type": "String", + "allowedValues": ["open", "closed", "opening", "closing"] + } + ], + "actions": [ + { + "name": "open" + }, + { + "name": "close" + }, + { + "name": "stop" + } + ] +} diff --git a/libguh/interfaces/interfaces.qrc b/libguh/interfaces/interfaces.qrc new file mode 100644 index 00000000..99218d01 --- /dev/null +++ b/libguh/interfaces/interfaces.qrc @@ -0,0 +1,9 @@ + + + mediacontroller.json + light.json + dimmablelight.json + colorlight.json + garagegate.json + + diff --git a/libguh/interfaces/light.json b/libguh/interfaces/light.json new file mode 100644 index 00000000..af95d791 --- /dev/null +++ b/libguh/interfaces/light.json @@ -0,0 +1,9 @@ +{ + "states": [ + { + "name": "power", + "type": "bool" + } + + ] +} diff --git a/libguh/interfaces/mediacontroller.json b/libguh/interfaces/mediacontroller.json new file mode 100644 index 00000000..83a95843 --- /dev/null +++ b/libguh/interfaces/mediacontroller.json @@ -0,0 +1,46 @@ +{ + "states": [ + { + "name": "mute", + "type": "bool", + "writable": true + }, + { + "name": "volume", + "type": "int", + "minValue": 0, + "maxValue": 100, + "writable": true + }, + { + "name": "playbackStatus", + "type": "string", + "allowedValues": ["Playing", "Paused", "Stopped"], + "writable": true + } + + ], + "actions": [ + { + "name": "skipBack" + }, + { + "name": "rewind" + }, + { + "name": "stop" + }, + { + "name": "play" + }, + { + "name": "pause" + }, + { + "name": "fastForward" + }, + { + "name": "skipNext" + } + ] +} diff --git a/libguh/libguh.pro b/libguh/libguh.pro index 1024463d..fc4d2715 100644 --- a/libguh/libguh.pro +++ b/libguh/libguh.pro @@ -141,3 +141,9 @@ for(header, HEADERS) { eval(headers_$${path}.path = $${path}) eval(INSTALLS *= headers_$${path}) } + +DISTFILES += \ + interfaces/mediacontroller.json + +RESOURCES += \ + interfaces/interfaces.qrc diff --git a/libguh/plugin/deviceclass.cpp b/libguh/plugin/deviceclass.cpp index c42af452..fe25819d 100644 --- a/libguh/plugin/deviceclass.cpp +++ b/libguh/plugin/deviceclass.cpp @@ -437,6 +437,16 @@ void DeviceClass::setPairingInfo(const QString &pairingInfo) m_pairingInfo = pairingInfo; } +QStringList DeviceClass::interfaces() const +{ + return m_interfaces; +} + +void DeviceClass::setInterfaces(const QStringList &interfaces) +{ + m_interfaces = interfaces; +} + /*! Compare this \a deviceClass to another. This is effectively the same as calling a.id() == b.id(). Returns true if the ids match.*/ bool DeviceClass::operator==(const DeviceClass &deviceClass) const { diff --git a/libguh/plugin/deviceclass.h b/libguh/plugin/deviceclass.h index cc5d0506..a483e1e3 100644 --- a/libguh/plugin/deviceclass.h +++ b/libguh/plugin/deviceclass.h @@ -172,6 +172,9 @@ public: QString pairingInfo() const; void setPairingInfo(const QString &pairingInfo); + QStringList interfaces() const; + void setInterfaces(const QStringList &interfaces); + bool operator==(const DeviceClass &device) const; private: @@ -192,6 +195,7 @@ private: CreateMethods m_createMethods; SetupMethod m_setupMethod; QString m_pairingInfo; + QStringList m_interfaces; }; Q_DECLARE_OPERATORS_FOR_FLAGS(DeviceClass::CreateMethods) diff --git a/libguh/plugin/deviceplugin.cpp b/libguh/plugin/deviceplugin.cpp index 36f12e6a..85242645 100644 --- a/libguh/plugin/deviceplugin.cpp +++ b/libguh/plugin/deviceplugin.cpp @@ -149,6 +149,7 @@ #include #include #include +#include /*! DevicePlugin constructor. DevicePlugins will be instantiated by the DeviceManager, its \a parent. */ DevicePlugin::DevicePlugin(QObject *parent): @@ -463,6 +464,75 @@ QList DevicePlugin::supportedDevices() const } } + QStringList interfaces; + foreach (const QJsonValue &value, deviceClassObject.value("interfaces").toArray()) { + // TODO: Check interfaces for completeness + QVariantMap interfaceMap = loadInterface(value.toString()); + QVariantList states = interfaceMap.value("states").toList(); + + StateTypes stateTypes(deviceClass.stateTypes()); + ActionTypes actionTypes(deviceClass.actionTypes()); + EventTypes eventTypes(deviceClass.eventTypes()); + bool valid = true; + foreach (const QVariant &stateVariant, states) { + StateType stateType = stateTypes.findByName(stateVariant.toMap().value("name").toString()); + QVariantMap stateMap = stateVariant.toMap(); + if (stateType.id().isNull()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but doesn't implement state" << stateMap.value("name").toString(); + valid = false; + continue; + } + if (QVariant::nameToType(stateMap.value("type").toByteArray().data()) != stateType.type()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but state" << stateMap.value("name").toString() << "has not matching type" << stateMap.value("type").toString(); + valid = false; + continue; + } + if (stateMap.contains("minimumValue") && stateMap.value("minimumValue") != stateType.minValue()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but state" << stateMap.value("name").toString() << "has not matching minimum value" << stateMap.value("minimumValue") << "!=" << stateType.minValue(); + valid = false; + continue; + } + if (stateMap.contains("maximumValue") && stateMap.value("maximumValue") != stateType.maxValue()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but state" << stateMap.value("name").toString() << "has not matching allowed value" << stateMap.value("maximumValue") << "!=" << stateType.maxValue(); + valid = false; + continue; + } + if (stateMap.contains("allowedValues") && stateMap.value("allowedValues") != stateType.possibleValues()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but state" << stateMap.value("name").toString() << "has not matching allowed values" << stateMap.value("allowedValues") << "!=" << stateType.possibleValues(); + valid = false; + continue; + } + if (stateMap.contains("writable") && stateMap.value("writable").toBool() && actionTypes.findById(ActionTypeId(stateType.id().toString())).id().isNull()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but state" << stateMap.value("name").toString() << "is not writable while it should be"; + valid = false; + continue; + } + } + QVariantList actions = interfaceMap.value("actions").toList(); + foreach (const QVariant &actionVariant, actions) { + QVariantMap actionMap = actionVariant.toMap(); + if (actionTypes.findByName(actionMap.value("name").toString()).id().isNull()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but doesn't implement action" << actionMap.value("name").toString(); + valid = false; + } + // TODO: check params + } + QVariantList events = interfaceMap.value("events").toList(); + foreach (const QVariant &eventVariant, events) { + QVariantMap eventMap = eventVariant.toMap(); + if (eventTypes.findByName(eventMap.value("name").toString()).id().isNull()) { + qCWarning(dcDeviceManager) << "DeviceClass" << deviceClass.name() << "claims to implement interface" << value.toString() << "but doesn't implement event" << eventMap.value("name").toString(); + valid = false; + } + // TODO: check params + } + + if (valid) { + interfaces.append(value.toString()); + } + } + deviceClass.setInterfaces(interfaces); + if (!broken) { deviceClasses.append(deviceClass); } else { @@ -1027,3 +1097,35 @@ QPair DevicePlugin::loadAndVerifyDeviceIcon(const return QPair(true, (DeviceClass::DeviceIcon)enumValue); } + +QVariantMap DevicePlugin::loadInterface(const QString &name) const +{ + QFile f(QString(":/interfaces/%1.json").arg(name)); + if (!f.open(QFile::ReadOnly)) { + qCWarning(dcDeviceManager()) << "Failed to load interface" << name; + return QVariantMap(); + } + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(f.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcDeviceManager) << "Cannot load interface definition for interface" << name << ":" << error.errorString(); + return QVariantMap(); + } + QVariantMap content = jsonDoc.toVariant().toMap(); + if (content.contains("extends")) { + QVariantMap parentContent = loadInterface(content.value("extends").toString()); + + QVariantList statesList = content.value("states").toList(); + statesList.append(parentContent.value("states").toList()); + content["states"] = statesList; + + QVariantList actionsList = content.value("actions").toList(); + actionsList.append(parentContent.value("actions").toList()); + content["actions"] = actionsList; + + QVariantList eventsList = content.value("events").toList(); + eventsList.append(parentContent.value("events").toList()); + content["events"] = eventsList; + } + return content; +} diff --git a/libguh/plugin/deviceplugin.h b/libguh/plugin/deviceplugin.h index 97ad514e..5afbb865 100644 --- a/libguh/plugin/deviceplugin.h +++ b/libguh/plugin/deviceplugin.h @@ -146,6 +146,8 @@ private: QPair loadAndVerifyBasicTag(const QString &basicTag) const; QPair loadAndVerifyDeviceIcon(const QString &deviceIcon) const; + QVariantMap loadInterface(const QString &name) const; + QTranslator *m_translator; DeviceManager *m_deviceManager; diff --git a/libguh/types/actiontype.cpp b/libguh/types/actiontype.cpp index 98e81b9c..bbe34188 100644 --- a/libguh/types/actiontype.cpp +++ b/libguh/types/actiontype.cpp @@ -91,3 +91,30 @@ void ActionType::setParamTypes(const QList ¶mTypes) { m_paramTypes = paramTypes; } + +ActionTypes::ActionTypes(const QList &other) +{ + foreach (const ActionType &at, other) { + append(at); + } +} + +ActionType ActionTypes::findByName(const QString &name) +{ + foreach (const ActionType &actionType, *this) { + if (actionType.name() == name) { + return actionType; + } + } + return ActionType(ActionTypeId()); +} + +ActionType ActionTypes::findById(const ActionTypeId &id) +{ + foreach (const ActionType &actionType, *this) { + if (actionType.id() == id) { + return actionType; + } + } + return ActionType(ActionTypeId()); +} diff --git a/libguh/types/actiontype.h b/libguh/types/actiontype.h index 1c8684cb..482438d4 100644 --- a/libguh/types/actiontype.h +++ b/libguh/types/actiontype.h @@ -53,4 +53,12 @@ private: QList m_paramTypes; }; +class ActionTypes: public QList +{ +public: + ActionTypes(const QList &other); + ActionType findByName(const QString &name); + ActionType findById(const ActionTypeId &id); +}; + #endif // ACTIONTYPE_H diff --git a/libguh/types/eventtype.cpp b/libguh/types/eventtype.cpp index 145d0aa3..2bd50a82 100644 --- a/libguh/types/eventtype.cpp +++ b/libguh/types/eventtype.cpp @@ -111,3 +111,30 @@ void EventType::setGraphRelevant(const bool &graphRelevant) { m_graphRelevant = graphRelevant; } + +EventTypes::EventTypes(const QList &other) +{ + foreach (const EventType &at, other) { + append(at); + } +} + +EventType EventTypes::findByName(const QString &name) +{ + foreach (const EventType &eventType, *this) { + if (eventType.name() == name) { + return eventType; + } + } + return EventType(EventTypeId()); +} + +EventType EventTypes::findById(const EventTypeId &id) +{ + foreach (const EventType &eventType, *this) { + if (eventType.id() == id) { + return eventType; + } + } + return EventType(EventTypeId()); +} diff --git a/libguh/types/eventtype.h b/libguh/types/eventtype.h index 63c85f3e..87323c46 100644 --- a/libguh/types/eventtype.h +++ b/libguh/types/eventtype.h @@ -61,4 +61,12 @@ private: bool m_graphRelevant; }; +class EventTypes: public QList +{ +public: + EventTypes(const QList &other); + EventType findByName(const QString &name); + EventType findById(const EventTypeId &id); +}; + #endif // TRIGGERTYPE_H diff --git a/libguh/types/statetype.cpp b/libguh/types/statetype.cpp index b8004fbd..c2062d99 100644 --- a/libguh/types/statetype.cpp +++ b/libguh/types/statetype.cpp @@ -179,3 +179,31 @@ void StateType::setGraphRelevant(const bool &graphRelevant) { m_graphRelevant = graphRelevant; } + + +StateTypes::StateTypes(const QList &other) +{ + foreach (const StateType &st, other) { + append(st); + } +} + +StateType StateTypes::findByName(const QString &name) +{ + foreach (const StateType &stateType, *this) { + if (stateType.name() == name) { + return stateType; + } + } + return StateType(StateTypeId()); +} + +StateType StateTypes::findById(const StateTypeId &id) +{ + foreach (const StateType &stateType, *this) { + if (stateType.id() == id) { + return stateType; + } + } + return StateType(StateTypeId()); +} diff --git a/libguh/types/statetype.h b/libguh/types/statetype.h index 38dfdf54..4036f636 100644 --- a/libguh/types/statetype.h +++ b/libguh/types/statetype.h @@ -82,4 +82,12 @@ private: }; +class StateTypes: public QList +{ +public: + StateTypes(const QList &other); + StateType findByName(const QString &name); + StateType findById(const StateTypeId &id); +}; + #endif // STATETYPE_H diff --git a/plugins/deviceplugins/deviceplugins.pro b/plugins/deviceplugins/deviceplugins.pro index 9db2356d..eb3bee68 100644 --- a/plugins/deviceplugins/deviceplugins.pro +++ b/plugins/deviceplugins/deviceplugins.pro @@ -1,35 +1,38 @@ TEMPLATE = subdirs SUBDIRS += \ - mock \ - elro \ - intertechno \ - networkdetector \ - conrad \ - openweathermap \ - lircd \ - wakeonlan \ - mailnotification \ +# mock \ +# elro \ +# intertechno \ +# networkdetector \ +# conrad \ +# openweathermap \ +# lircd \ +# wakeonlan \ +# mailnotification \ philipshue \ - lgsmarttv \ - datetime \ - genericelements \ - commandlauncher \ - unitec \ - leynew \ - udpcommander \ +# eq-3 \ +# wemo \ +# lgsmarttv \ +# datetime \ +# genericelements \ +# commandlauncher \ +# unitec \ +# leynew \ +# udpcommander \ kodi \ - elgato \ - awattar \ - netatmo \ - dollhouse \ - plantcare \ - osdomotics \ - ws2812 \ - orderbutton \ - denon \ - avahimonitor \ - senic \ - gpio \ +# elgato \ +# awattar \ +# netatmo \ +# dollhouse \ +# plantcare \ +# osdomotics \ +# ws2812 \ +# orderbutton \ +# denon \ +# avahimonitor \ +# usbwde \ +# senic \ +# gpio \ disabletesting { SUBDIRS -= mock diff --git a/plugins/deviceplugins/kodi/devicepluginkodi.json b/plugins/deviceplugins/kodi/devicepluginkodi.json index ad62ea8b..3ca9da61 100644 --- a/plugins/deviceplugins/kodi/devicepluginkodi.json +++ b/plugins/deviceplugins/kodi/devicepluginkodi.json @@ -13,6 +13,7 @@ "idName": "kodi", "name": "Kodi", "deviceIcon": "Tv", + "interfaces": ["mediacontroller"], "basicTags": [ "Service", "Multimedia", diff --git a/plugins/deviceplugins/philipshue/devicepluginphilipshue.json b/plugins/deviceplugins/philipshue/devicepluginphilipshue.json index 30b1a55c..fc8a628f 100644 --- a/plugins/deviceplugins/philipshue/devicepluginphilipshue.json +++ b/plugins/deviceplugins/philipshue/devicepluginphilipshue.json @@ -158,6 +158,7 @@ "idName": "hueLight", "name": "Hue Light", "deviceIcon": "LightBulb", + "interfaces": ["colorlight"], "basicTags": [ "Device", "Lighting", diff --git a/server/jsonrpc/jsontypes.cpp b/server/jsonrpc/jsontypes.cpp index 11d4191b..895162ec 100644 --- a/server/jsonrpc/jsontypes.cpp +++ b/server/jsonrpc/jsontypes.cpp @@ -250,6 +250,7 @@ void JsonTypes::init() s_deviceClass.insert("pluginId", basicTypeToString(Uuid)); s_deviceClass.insert("name", basicTypeToString(String)); s_deviceClass.insert("deviceIcon", deviceIconRef()); + s_deviceClass.insert("interfaces", QVariantList() << basicTypeToString(String)); s_deviceClass.insert("basicTags", QVariantList() << basicTagRef()); s_deviceClass.insert("setupMethod", setupMethodRef()); s_deviceClass.insert("createMethods", QVariantList() << createMethodRef()); @@ -680,6 +681,7 @@ QVariantMap JsonTypes::packDeviceClass(const DeviceClass &deviceClass) variant.insert("vendorId", deviceClass.vendorId().toString()); variant.insert("pluginId", deviceClass.pluginId().toString()); variant.insert("deviceIcon", s_deviceIcon.at(deviceClass.deviceIcon())); + variant.insert("interfaces", deviceClass.interfaces()); QVariantList basicTags; foreach (const DeviceClass::BasicTag &basicTag, deviceClass.basicTags())