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
pull/135/head
Michael Zanetti 2017-07-10 12:12:41 +02:00
parent e9818e9caf
commit 45caf66851
22 changed files with 377 additions and 28 deletions

View File

@ -195,6 +195,7 @@
#include <QCoreApplication>
#include <QStandardPaths>
#include <QDir>
#include <QJsonDocument>
/*! 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. */

View File

@ -0,0 +1,15 @@
{
"extends": "dimmablelight",
"states": [
{
"name": "color temperature",
"type": "int",
"minValue": "any",
"maxValue": "any"
},
{
"name": "color",
"type": "QColor"
}
]
}

View File

@ -0,0 +1,12 @@
{
"extends": "light",
"states": [
{
"name": "brightness",
"type": "int",
"minimumValue": 0,
"maximumValue": 100,
"writable": true
}
]
}

View File

@ -0,0 +1,20 @@
{
"states": [
{
"name": "gateState",
"type": "String",
"allowedValues": ["open", "closed", "opening", "closing"]
}
],
"actions": [
{
"name": "open"
},
{
"name": "close"
},
{
"name": "stop"
}
]
}

View File

@ -0,0 +1,9 @@
<RCC>
<qresource prefix="/interfaces">
<file>mediacontroller.json</file>
<file>light.json</file>
<file>dimmablelight.json</file>
<file>colorlight.json</file>
<file>garagegate.json</file>
</qresource>
</RCC>

View File

@ -0,0 +1,9 @@
{
"states": [
{
"name": "power",
"type": "bool"
}
]
}

View File

@ -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"
}
]
}

View File

@ -141,3 +141,9 @@ for(header, HEADERS) {
eval(headers_$${path}.path = $${path})
eval(INSTALLS *= headers_$${path})
}
DISTFILES += \
interfaces/mediacontroller.json
RESOURCES += \
interfaces/interfaces.qrc

View File

@ -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
{

View File

@ -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)

View File

@ -149,6 +149,7 @@
#include <QDir>
#include <QCoreApplication>
#include <QJsonArray>
#include <QJsonDocument>
/*! DevicePlugin constructor. DevicePlugins will be instantiated by the DeviceManager, its \a parent. */
DevicePlugin::DevicePlugin(QObject *parent):
@ -463,6 +464,75 @@ QList<DeviceClass> 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<bool, DeviceClass::DeviceIcon> DevicePlugin::loadAndVerifyDeviceIcon(const
return QPair<bool, DeviceClass::DeviceIcon>(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;
}

View File

@ -146,6 +146,8 @@ private:
QPair<bool, DeviceClass::BasicTag> loadAndVerifyBasicTag(const QString &basicTag) const;
QPair<bool, DeviceClass::DeviceIcon> loadAndVerifyDeviceIcon(const QString &deviceIcon) const;
QVariantMap loadInterface(const QString &name) const;
QTranslator *m_translator;
DeviceManager *m_deviceManager;

View File

@ -91,3 +91,30 @@ void ActionType::setParamTypes(const QList<ParamType> &paramTypes)
{
m_paramTypes = paramTypes;
}
ActionTypes::ActionTypes(const QList<ActionType> &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());
}

View File

@ -53,4 +53,12 @@ private:
QList<ParamType> m_paramTypes;
};
class ActionTypes: public QList<ActionType>
{
public:
ActionTypes(const QList<ActionType> &other);
ActionType findByName(const QString &name);
ActionType findById(const ActionTypeId &id);
};
#endif // ACTIONTYPE_H

View File

@ -111,3 +111,30 @@ void EventType::setGraphRelevant(const bool &graphRelevant)
{
m_graphRelevant = graphRelevant;
}
EventTypes::EventTypes(const QList<EventType> &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());
}

View File

@ -61,4 +61,12 @@ private:
bool m_graphRelevant;
};
class EventTypes: public QList<EventType>
{
public:
EventTypes(const QList<EventType> &other);
EventType findByName(const QString &name);
EventType findById(const EventTypeId &id);
};
#endif // TRIGGERTYPE_H

View File

@ -179,3 +179,31 @@ void StateType::setGraphRelevant(const bool &graphRelevant)
{
m_graphRelevant = graphRelevant;
}
StateTypes::StateTypes(const QList<StateType> &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());
}

View File

@ -82,4 +82,12 @@ private:
};
class StateTypes: public QList<StateType>
{
public:
StateTypes(const QList<StateType> &other);
StateType findByName(const QString &name);
StateType findById(const StateTypeId &id);
};
#endif // STATETYPE_H

View File

@ -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

View File

@ -13,6 +13,7 @@
"idName": "kodi",
"name": "Kodi",
"deviceIcon": "Tv",
"interfaces": ["mediacontroller"],
"basicTags": [
"Service",
"Multimedia",

View File

@ -158,6 +158,7 @@
"idName": "hueLight",
"name": "Hue Light",
"deviceIcon": "LightBulb",
"interfaces": ["colorlight"],
"basicTags": [
"Device",
"Lighting",

View File

@ -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())