some more python plugin work

This commit is contained in:
Michael Zanetti 2020-06-29 14:55:42 +02:00
parent 5d0751ae27
commit c7f957f201
26 changed files with 840 additions and 437 deletions

View File

@ -53,10 +53,7 @@ CloudNotifications::CloudNotifications(AWSConnector* awsConnector, QObject *pare
connect(m_awsConnector, &AWSConnector::pushNotificationEndpointsUpdated, this, &CloudNotifications::pushNotificationEndpointsUpdated);
connect(m_awsConnector, &AWSConnector::pushNotificationEndpointAdded, this, &CloudNotifications::pushNotificationEndpointAdded);
connect(m_awsConnector, &AWSConnector::pushNotificationSent, this, &CloudNotifications::pushNotificationSent);
}
PluginMetadata CloudNotifications::metaData() const
{
QVariantMap pluginMetaData;
pluginMetaData.insert("id", "ccc6dbc8-e352-48a1-8e87-3c89a4669fc2");
pluginMetaData.insert("name", "CloudNotifications");
@ -145,7 +142,8 @@ PluginMetadata CloudNotifications::metaData() const
vendors.append(guhVendor);
pluginMetaData.insert("vendors", vendors);
return PluginMetadata(QJsonObject::fromVariantMap(pluginMetaData), true);
setMetaData(PluginMetadata(QJsonObject::fromVariantMap(pluginMetaData), true));
}
void CloudNotifications::setupThing(ThingSetupInfo *info)

View File

@ -2,7 +2,9 @@
#define PYPARAM_H
#include <Python.h>
#include "structmember.h"
#include <structmember.h>
#include "pyutils.h"
#include "types/param.h"
@ -20,8 +22,6 @@ typedef struct _pyparam {
} PyParam;
static void PyParam_dealloc(PyParam * self) {
// FIXME: Why is this not called? Seems we're leaking...
Q_ASSERT(false);
Py_XDECREF(self->pyParamTypeId);
Py_XDECREF(self->pyValue);
Py_TYPE(self)->tp_free(self);
@ -88,38 +88,18 @@ static PyParam* PyParam_fromParam(const Param &param)
{
PyParam *pyParam = PyObject_New(PyParam, &PyParamType);
pyParam->pyParamTypeId = PyUnicode_FromString(param.paramTypeId().toString().toUtf8());
switch (param.value().type()) {
case QVariant::Bool:
pyParam->pyValue = PyBool_FromLong(*(long*)param.value().data());
break;
case QVariant::Int:
case QVariant::UInt:
case QVariant::LongLong:
case QVariant::ULongLong:
pyParam->pyValue = PyLong_FromLong(*(long*)param.value().data());
break;
case QVariant::String:
case QVariant::ByteArray:
pyParam->pyValue = PyUnicode_FromString(param.value().toString().toUtf8());
break;
case QVariant::Double:
pyParam->pyValue = PyFloat_FromDouble(param.value().toDouble());
break;
case QVariant::Invalid:
pyParam->pyValue = Py_None;
Py_INCREF(pyParam->pyValue);
break;
default:
qCWarning(dcThingManager) << "Unhandled data type in conversion from Param to PyParam!";
pyParam->pyValue = Py_None;
Py_INCREF(pyParam->pyValue);
break;
}
pyParam->pyValue = QVariantToPyObject(param.value());
return pyParam;
}
static PyObject* PyParam_FromParamList(const ParamList &params)
static Param PyParam_ToParam(PyParam *pyParam)
{
ParamTypeId paramTypeId = ParamTypeId(PyUnicode_AsUTF8(pyParam->pyParamTypeId));
QVariant value = PyObjectToQVariant(pyParam->pyValue);
return Param(paramTypeId, value);
}
static PyObject* PyParams_FromParamList(const ParamList &params)
{
PyObject* result = PyTuple_New(params.count());
for (int i = 0; i < params.count(); i++) {
@ -128,6 +108,31 @@ static PyObject* PyParam_FromParamList(const ParamList &params)
return result;
}
static ParamList PyParams_ToParamList(PyObject *pyParams)
{
ParamList params;
if (pyParams != nullptr) {
return params;
}
PyObject *iter = PyObject_GetIter(pyParams);
while (iter) {
PyObject *next = PyIter_Next(iter);
if (!next) {
break;
}
if (next->ob_type != &PyParamType) {
qCWarning(dcThingManager()) << "Invalid parameter passed in param list";
continue;
}
PyParam *pyParam = reinterpret_cast<PyParam*>(next);
params.append(PyParam_ToParam(pyParam));
}
return params;
}
static void registerParamType(PyObject *module)
{

View File

@ -17,10 +17,22 @@
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
#pragma GCC diagnostic ignored "-Wwrite-strings"
/* Note: Thing is not threadsafe so we must never access thing directly in here.
* For writing access, invoking with QueuedConnections will decouple stuff
* For reading access, we keep a cache of the thing properties here and sync them
* over to the cache when they change.
* When using this, make sure to call PyThing_setThing() after constructing it.
*/
typedef struct _thing {
PyObject_HEAD
Thing *thing;
QMutex *mutex;
Thing *thing = nullptr;
PyObject *name = nullptr;
PyObject *params = nullptr;
PyObject *settings = nullptr;
PyObject *nameChangedHandler = nullptr;
QMutex *mutex = nullptr;
} PyThing;
@ -30,13 +42,69 @@ static PyObject* PyThing_new(PyTypeObject *type, PyObject */*args*/, PyObject */
return nullptr;
}
self->mutex = new QMutex();
return (PyObject*)self;
}
static void PyThing_setThing(PyThing *self, Thing *thing)
{
self->thing = thing;
self->name = PyUnicode_FromString(self->thing->name().toUtf8().data());
Py_INCREF(self->name);
QObject::connect(thing, &Thing::nameChanged, [=](){
self->mutex->lock();
Py_XDECREF(self->name);
self->name = PyUnicode_FromString(self->thing->name().toUtf8().data());
if (!self->nameChangedHandler) {
self->mutex->unlock();
return;
}
self->mutex->unlock();
PyGILState_STATE s = PyGILState_Ensure();
PyObject_CallFunctionObjArgs(self->nameChangedHandler, self, nullptr);
if (PyErr_Occurred()) {
PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
if (pvalue) {
PyObject *pstr = PyObject_Str(pvalue);
if (pstr) {
const char* err_msg = PyUnicode_AsUTF8(pstr);
if (pstr) {
qCWarning(dcThingManager()) << QString(err_msg);
}
}
PyErr_Restore(ptype, pvalue, ptraceback);
}
}
PyGILState_Release(s);
});
self->params = PyParams_FromParamList(self->thing->params());
Py_INCREF(self->params);
self->settings = PyParams_FromParamList(self->thing->settings());
Py_INCREF(self->settings);
QObject::connect(thing, &Thing::settingChanged, [=](){
QMutexLocker(self->mutex);
Py_XDECREF(self->settings);
self->settings = PyParams_FromParamList(self->thing->settings());
});
}
static void PyThing_dealloc(PyThing * self) {
Py_TYPE(self)->tp_free(self);
Py_XDECREF(self->name);
Py_XDECREF(self->params);
Py_XDECREF(self->settings);
Py_XDECREF(self->nameChangedHandler);
delete self->mutex;
Py_TYPE(self)->tp_free(self);
}
static PyObject *PyThing_getName(PyThing *self, void */*closure*/)
@ -46,11 +114,9 @@ static PyObject *PyThing_getName(PyThing *self, void */*closure*/)
PyErr_SetString(PyExc_ValueError, "Thing has been removed from the system.");
return nullptr;
}
QString name;
QMetaObject::invokeMethod(self->thing, "name", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, name));
PyObject *ret = PyUnicode_FromString(name.toUtf8().data());
Py_INCREF(ret);
return ret;
Py_INCREF(self->name);
return self->name;
}
static int PyThing_setName(PyThing *self, PyObject *value, void */*closure*/){
@ -67,12 +133,8 @@ static PyObject *PyThing_getSettings(PyThing *self, void */*closure*/)
PyErr_SetString(PyExc_ValueError, "Thing has been removed from the system.");
return nullptr;
}
qWarning() << "setting thread" << QThread::currentThread();
ParamList settings;
QMetaObject::invokeMethod(self->thing, "settings", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ParamList, settings));
PyObject *ret = PyParam_FromParamList(settings);
Py_INCREF(ret);
return ret;
Py_INCREF(self->settings);
return self->settings;
}
static int PyThing_setSettings(PyThing */*self*/, PyObject */*value*/, void */*closure*/){
@ -85,45 +147,22 @@ static PyObject * PyThing_setStateValue(PyThing* self, PyObject* args)
char *stateTypeIdStr = nullptr;
PyObject *valueObj = nullptr;
// FIXME: is there any better way to do this? Value is a variant
if (!PyArg_ParseTuple(args, "sO", &stateTypeIdStr, &valueObj)) {
qCWarning(dcThingManager) << "Error parsing parameters";
return nullptr;
}
StateTypeId stateTypeId = StateTypeId(stateTypeIdStr);
PyObject* repr = PyObject_Repr(valueObj);
PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
const char *bytes = PyBytes_AS_STRING(str);
QVariant value(bytes);
QVariant value = PyObjectToQVariant(valueObj);
QMutexLocker(self->mutex);
if (self->thing != nullptr) {
QMetaObject::invokeMethod(self->thing, "setStateValue", Qt::QueuedConnection, Q_ARG(StateTypeId, stateTypeId), Q_ARG(QVariant, value));
}
Py_XDECREF(repr);
Py_XDECREF(str);
Py_RETURN_NONE;
}
static PyObject *PyThing_settingChanged(PyThing *self, void */*closure*/)
{
QMutexLocker(self->mutex);
if (!self->thing) {
PyErr_SetString(PyExc_ValueError, "Thing has been removed from the system.");
return nullptr;
}
ParamList settings;
QMetaObject::invokeMethod(self->thing, "settings", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ParamList, settings));
PyObject *ret = PyParam_FromParamList(settings);
Py_INCREF(ret);
return ret;
}
static PyObject * PyThing_emitEvent(PyThing* self, PyObject* args)
{
char *eventTypeIdStr = nullptr;
@ -135,35 +174,7 @@ static PyObject * PyThing_emitEvent(PyThing* self, PyObject* args)
}
EventTypeId eventTypeId = EventTypeId(eventTypeIdStr);
ParamList params;
if (valueObj != nullptr) {
PyObject *iter = PyObject_GetIter(valueObj);
while (iter) {
PyObject *next = PyIter_Next(iter);
if (!next) {
break;
}
if (next->ob_type != &PyParamType) {
qCWarning(dcThingManager()) << "Invalid parameter passed in param list";
continue;
}
PyParam *pyParam = reinterpret_cast<PyParam*>(next);
ParamTypeId paramTypeId = ParamTypeId(PyUnicode_AsUTF8(pyParam->pyParamTypeId));
// Is there a better way to convert a PyObject to a QVariant?
PyObject* repr = PyObject_Repr(pyParam->pyValue);
PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
const char *bytes = PyBytes_AS_STRING(str);
Py_XDECREF(repr);
Py_XDECREF(str);
QVariant value(bytes);
params.append(Param(paramTypeId, value));
}
}
ParamList params = PyParams_ToParamList(valueObj);
QMutexLocker(self->mutex);
if (self->thing != nullptr) {
@ -176,7 +187,6 @@ static PyObject * PyThing_emitEvent(PyThing* self, PyObject* args)
static PyGetSetDef PyThing_getset[] = {
{"name", (getter)PyThing_getName, (setter)PyThing_setName, "Thing name", nullptr},
{"settings", (getter)PyThing_getSettings, (setter)PyThing_setSettings, "Thing settings", nullptr},
{"settingChanged", (getter)PyThing_settingChanged, nullptr, "Signal for changed settings", nullptr},
{nullptr , nullptr, nullptr, nullptr, nullptr} /* Sentinel */
};
@ -186,6 +196,11 @@ static PyMethodDef PyThing_methods[] = {
{nullptr, nullptr, 0, nullptr} // sentinel
};
static PyMemberDef PyThing_members[] = {
{"nameChangedHandler", T_OBJECT_EX, offsetof(PyThing, nameChangedHandler), 0, "Set a callback for when the thing name changes"},
{nullptr, 0, 0, 0, nullptr} /* Sentinel */
};
static PyTypeObject PyThingType = {
PyVarObject_HEAD_INIT(NULL, 0)
"nymea.Thing", /* tp_name */
@ -215,7 +230,7 @@ static PyTypeObject PyThingType = {
0, /* tp_iter */
0, /* tp_iternext */
PyThing_methods, /* tp_methods */
0, /* tp_members */
PyThing_members, /* tp_members */
PyThing_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */

View File

@ -18,15 +18,23 @@ typedef struct {
PyThing *pyThing;
PyObject *pyActionTypeId;
PyObject *pyParams;
QMutex *mutex;
} PyThingActionInfo;
static int PyThingActionInfo_init(PyThingActionInfo */*self*/, PyObject */*args*/, PyObject */*kwds*/) {
return 0;
static PyObject* PyThingActionInfo_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) {
PyThingActionInfo *self = (PyThingActionInfo*)type->tp_alloc(type, 0);
if (self == NULL) {
return nullptr;
}
self->mutex = new QMutex();
return (PyObject*)self;
}
static void PyThingActionInfo_dealloc(PyThingActionInfo * self) {
static void PyThingActionInfo_dealloc(PyThingActionInfo * self)
{
delete self->mutex;
Py_TYPE(self)->tp_free(self);
}
@ -63,39 +71,59 @@ static PyMethodDef PyThingActionInfo_methods[] = {
static PyTypeObject PyThingActionInfoType = {
PyVarObject_HEAD_INIT(NULL, 0)
"nymea.ThingActionInfo", /* tp_name */
sizeof(PyThingActionInfo), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"Noddy objects", /* tp_doc */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
"nymea.ThingActionInfo", /* tp_name */
sizeof(PyThingActionInfo), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)PyThingActionInfo_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"ThingActionInfo", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
PyThingActionInfo_methods, /* tp_methods */
PyThingActionInfo_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
(newfunc)PyThingActionInfo_new, /* tp_new */
0, /* tp_free */
0, /* tp_is_gc */
0, /* tp_bases */
0, /* tp_mro */
0, /* tp_cache */
0, /* tp_subclasses */
0, /* tp_weaklist */
0, /* tp_del */
0, /* tp_version_tag */
0, /* tp_finalize */
0, /* tp_vectorcall */
0, /* tp_print DEPRECATED*/
};
static void registerThingActionInfoType(PyObject *module) {
PyThingActionInfoType.tp_new = PyType_GenericNew;
PyThingActionInfoType.tp_dealloc=(destructor) PyThingActionInfo_dealloc;
PyThingActionInfoType.tp_basicsize = sizeof(PyThingActionInfo);
PyThingActionInfoType.tp_flags = Py_TPFLAGS_DEFAULT;
PyThingActionInfoType.tp_doc = "ThingActionInfo class";
PyThingActionInfoType.tp_methods = PyThingActionInfo_methods;
PyThingActionInfoType.tp_members = PyThingActionInfo_members;
PyThingActionInfoType.tp_init = (initproc)PyThingActionInfo_init;
static void registerThingActionInfoType(PyObject *module)
{
if (PyType_Ready(&PyThingActionInfoType) < 0) {
return;
}

View File

@ -18,10 +18,6 @@ typedef struct {
PyObject* pyDescription;
} PyThingDescriptor;
static PyMethodDef PyThingDescriptor_methods[] = {
// { "finish", (PyCFunction)PyThingDiscoveryInfo_finish, METH_VARARGS, "finish a discovery" },
{nullptr, nullptr, 0, nullptr} // sentinel
};
static PyMemberDef PyThingDescriptor_members[] = {
{"thingClassId", T_OBJECT_EX, offsetof(PyThingDescriptor, pyThingClassId), 0, "Descriptor thingClassId"},
@ -30,7 +26,6 @@ static PyMemberDef PyThingDescriptor_members[] = {
{nullptr, 0, 0, 0, nullptr} /* Sentinel */
};
static int PyThingDescriptor_init(PyThingDescriptor *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"thingClassId", "name", "description", nullptr};
@ -57,14 +52,6 @@ static int PyThingDescriptor_init(PyThingDescriptor *self, PyObject *args, PyObj
return 0;
}
//static PyGetSetDef PyThingDescriptor_getsetters[] = {
// {"name", (getter) PyThingDescriptor_getName, (setter) PyThingDescriptir_setName,
// "Descriptor name", NULL},
// {"last", (getter) Custom_getlast, (setter) Custom_setlast,
// "last name", NULL},
// {NULL} /* Sentinel */
//};
static PyTypeObject PyThingDescriptorType = {
PyVarObject_HEAD_INIT(NULL, 0)
"nymea.ThingDescriptor", /* tp_name */
@ -86,7 +73,7 @@ static PyTypeObject PyThingDescriptorType = {
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"Noddy objects", /* tp_doc */
"ThingDescriptor", /* tp_doc */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
@ -95,10 +82,6 @@ static PyTypeObject PyThingDescriptorType = {
static void registerThingDescriptorType(PyObject *module)
{
PyThingDescriptorType.tp_new = PyType_GenericNew;
PyThingDescriptorType.tp_basicsize = sizeof(PyThingDescriptor);
PyThingDescriptorType.tp_flags = Py_TPFLAGS_DEFAULT;
PyThingDescriptorType.tp_doc = "ThingDescriptor class";
PyThingDescriptorType.tp_methods = PyThingDescriptor_methods;
PyThingDescriptorType.tp_members = PyThingDescriptor_members;
PyThingDescriptorType.tp_init = reinterpret_cast<initproc>(PyThingDescriptor_init);

View File

@ -11,6 +11,7 @@
#include <QDebug>
#include <QMetaEnum>
#include <QMutex>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
@ -20,17 +21,24 @@
typedef struct {
PyObject_HEAD
ThingDiscoveryInfo* info;
QMutex *mutex;
} PyThingDiscoveryInfo;
static int PyThingDiscoveryInfo_init(PyThingDiscoveryInfo */*self*/, PyObject */*args*/, PyObject */*kwds*/) {
return 0;
static PyObject* PyThingDiscoveryInfo_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/)
{
PyThingDiscoveryInfo *self = (PyThingDiscoveryInfo*)type->tp_alloc(type, 0);
if (self == NULL) {
return nullptr;
}
self->mutex = new QMutex();
return (PyObject*)self;
}
static void PyThingDiscoveryInfo_dealloc(PyThingDiscoveryInfo * self) {
static void PyThingDiscoveryInfo_dealloc(PyThingDiscoveryInfo * self)
{
// FIXME: Why is this not called? Seems we're leaking...
Q_ASSERT(false);
delete self->mutex;
Py_TYPE(self)->tp_free(self);
}
@ -53,8 +61,8 @@ static PyObject * PyThingDiscoveryInfo_finish(PyThingDiscoveryInfo* self, PyObje
Py_RETURN_NONE;
}
static PyObject * PyThingDiscoveryInfo_addDescriptor(PyThingDiscoveryInfo* self, PyObject* args) {
static PyObject * PyThingDiscoveryInfo_addDescriptor(PyThingDiscoveryInfo* self, PyObject* args)
{
PyObject *pyObj = nullptr;
if (!PyArg_ParseTuple(args, "O", &pyObj)) {
@ -90,47 +98,68 @@ static PyObject * PyThingDiscoveryInfo_addDescriptor(PyThingDiscoveryInfo* self,
}
static PyMethodDef PyThingDiscoveryInfo_methods[] = {
{ "addDescriptor", (PyCFunction)PyThingDiscoveryInfo_addDescriptor, METH_VARARGS, "Add a new descriptor to the discovery" },
{ "finish", (PyCFunction)PyThingDiscoveryInfo_finish, METH_VARARGS, "finish a discovery" },
{ "addDescriptor", (PyCFunction)PyThingDiscoveryInfo_addDescriptor, METH_VARARGS, "Add a new descriptor to the discovery" },
{ "finish", (PyCFunction)PyThingDiscoveryInfo_finish, METH_VARARGS, "Finish a discovery" },
{nullptr, nullptr, 0, nullptr} // sentinel
};
static PyTypeObject PyThingDiscoveryInfoType = {
PyVarObject_HEAD_INIT(NULL, 0)
"nymea.ThingDiscoveryInfo", /* tp_name */
"nymea.ThingDiscoveryInfo", /* tp_name */
sizeof(PyThingDiscoveryInfo), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"Noddy objects", /* tp_doc */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0, /* tp_itemsize */
(destructor)PyThingDiscoveryInfo_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"ThingDiscoveryInfo", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
PyThingDiscoveryInfo_methods, /* tp_methods */
0, //PyThingDiscoveryInfo_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
(newfunc)PyThingDiscoveryInfo_new, /* tp_new */
0, /* tp_free */
0, /* tp_is_gc */
0, /* tp_bases */
0, /* tp_mro */
0, /* tp_cache */
0, /* tp_subclasses */
0, /* tp_weaklist */
0, /* tp_del */
0, /* tp_version_tag */
0, /* tp_finalize */
0, /* tp_vectorcall */
0, /* tp_print DEPRECATED*/
};
static void registerThingDiscoveryInfoType(PyObject *module)
{
PyThingDiscoveryInfoType.tp_new = PyType_GenericNew;
PyThingDiscoveryInfoType.tp_basicsize = sizeof(PyThingDiscoveryInfo);
PyThingDiscoveryInfoType.tp_dealloc = reinterpret_cast<destructor>(PyThingDiscoveryInfo_dealloc);
PyThingDiscoveryInfoType.tp_flags = Py_TPFLAGS_DEFAULT;
PyThingDiscoveryInfoType.tp_doc = "ThingDiscoveryInfo class";
PyThingDiscoveryInfoType.tp_methods = PyThingDiscoveryInfo_methods;
PyThingDiscoveryInfoType.tp_init = reinterpret_cast<initproc>(PyThingDiscoveryInfo_init);
if (PyType_Ready(&PyThingDiscoveryInfoType) < 0) {
return;

View File

@ -30,8 +30,8 @@ static PyObject* PyThingSetupInfo_new(PyTypeObject *type, PyObject */*args*/, Py
}
static void PyThingSetupInfo_dealloc(PyThingSetupInfo * self) {
Py_TYPE(self)->tp_free(self);
delete self->mutex;
Py_TYPE(self)->tp_free(self);
}
static PyObject * PyThingSetupInfo_finish(PyThingSetupInfo* self, PyObject* args) {
@ -117,7 +117,8 @@ static PyTypeObject PyThingSetupInfoType = {
0, /* tp_print DEPRECATED*/
};
static void registerThingSetupInfoType(PyObject *module) {
static void registerThingSetupInfoType(PyObject *module)
{
if (PyType_Ready(&PyThingSetupInfoType) < 0) {
return;
}

View File

@ -0,0 +1,61 @@
#ifndef PYUTILS_H
#define PYUTILS_H
#include <Python.h>
#include "loggingcategories.h"
#include <QVariant>
/* Returns a PyObject. RefCount will be 1 */
PyObject *QVariantToPyObject(const QVariant &value)
{
PyObject *pyValue = nullptr;
switch (value.type()) {
case QVariant::Bool:
pyValue = PyBool_FromLong(*(long*)value.data());
break;
case QVariant::Int:
case QVariant::UInt:
case QVariant::LongLong:
case QVariant::ULongLong:
pyValue = PyLong_FromLong(*(long*)value.data());
break;
case QVariant::String:
case QVariant::ByteArray:
pyValue = PyUnicode_FromString(value.toString().toUtf8());
break;
case QVariant::Double:
pyValue = PyFloat_FromDouble(value.toDouble());
break;
case QVariant::Invalid:
pyValue = Py_None;
Py_INCREF(pyValue);
break;
default:
qCWarning(dcThingManager) << "Unhandled data type in conversion from Param to PyParam!";
pyValue = Py_None;
Py_INCREF(pyValue);
break;
}
return pyValue;
}
QVariant PyObjectToQVariant(PyObject *pyObject)
{
// FIXME: is there any better way to do this?
PyObject* repr = PyObject_Repr(pyObject);
PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
const char *bytes = PyBytes_AS_STRING(str);
QVariant value(bytes);
Py_XDECREF(repr);
Py_XDECREF(str);
return value;
}
#endif // PYUTILS_H

View File

@ -15,11 +15,14 @@
#include <QMetaEnum>
#include <QJsonDocument>
#include <QtConcurrent/QtConcurrentRun>
#include <QCoreApplication>
#include <QMutex>
PyThreadState* PythonIntegrationPlugin::s_mainThread = nullptr;
PyObject* PythonIntegrationPlugin::s_nymeaModule = nullptr;
PyObject* PythonIntegrationPlugin::s_asyncio = nullptr;
QHash<PythonIntegrationPlugin*, PyObject*> PythonIntegrationPlugin::s_plugins;
// Write to stdout/stderr
PyObject* nymea_write(PyObject* /*self*/, PyObject* args)
@ -106,6 +109,147 @@ PyMODINIT_FUNC PyInit_nymea(void)
return m;
}
PyObject* PythonIntegrationPlugin::pyConfiguration(PyObject* self, PyObject* /*args*/)
{
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
plugin->m_mutex.lock();
PyObject *params = PyParams_FromParamList(plugin->configuration());
plugin->m_mutex.unlock();
return params;
}
PyObject *PythonIntegrationPlugin::pyConfigValue(PyObject *self, PyObject *args)
{
char *paramTypeIdStr = nullptr;
if (!PyArg_ParseTuple(args, "s", &paramTypeIdStr)) {
qCWarning(dcThingManager) << "Error parsing parameters";
return nullptr;
}
ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr);
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
plugin->m_mutex.lock();
QVariant value = plugin->m_pluginConfigCopy.paramValue(paramTypeId);
plugin->m_mutex.unlock();
return QVariantToPyObject(value);
}
PyObject *PythonIntegrationPlugin::pySetConfigValue(PyObject *self, PyObject *args)
{
char *paramTypeIdStr = nullptr;
PyObject *valueObj = nullptr;
if (!PyArg_ParseTuple(args, "sO", &paramTypeIdStr, &valueObj)) {
qCWarning(dcThingManager) << "Error parsing parameters";
return nullptr;
}
ParamTypeId paramTypeId = EventTypeId(paramTypeIdStr);
QVariant value = PyObjectToQVariant(valueObj);
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
QMetaObject::invokeMethod(plugin, "setConfigValue", Qt::QueuedConnection, Q_ARG(ParamTypeId, paramTypeId), Q_ARG(QVariant, value));
Py_RETURN_NONE;
}
PyObject *PythonIntegrationPlugin::pyMyThings(PyObject *self, PyObject */*args*/)
{
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
plugin->m_mutex.lock();
PyObject* result = PyTuple_New(plugin->m_things.count());
for (int i = 0; i < plugin->m_things.count(); i++) {
Thing *thing = plugin->m_things.keys().at(i);
PyTuple_SET_ITEM(result, i, (PyObject*)plugin->m_things.value(thing));
}
plugin->m_mutex.unlock();
return result;
}
PyObject *PythonIntegrationPlugin::pyAutoThingsAppeared(PyObject *self, PyObject *args)
{
PyObject *pyParams;
if (!PyArg_ParseTuple(args, "O", &pyParams)) {
qCWarning(dcThingManager()) << "Error parsing args. Not a param list";
return nullptr;
}
PyObject *iter = PyObject_GetIter(pyParams);
if (!iter) {
qCWarning(dcThingManager()) << "Error parsing args. Not a param list";
return nullptr;
}
ThingDescriptors descriptors;
while (true) {
PyObject *next = PyIter_Next(iter);
if (!next) {
// nothing left in the iterator
break;
}
if (next->ob_type != &PyThingDescriptorType) {
PyErr_SetString(PyExc_ValueError, "Invalid argument. Not a ThingDescriptor.");
return nullptr;
}
PyThingDescriptor *pyDescriptor = (PyThingDescriptor*)next;
ThingClassId thingClassId;
if (pyDescriptor->pyThingClassId) {
thingClassId = ThingClassId(PyUnicode_AsUTF8(pyDescriptor->pyThingClassId));
}
QString name;
if (pyDescriptor->pyName) {
name = QString::fromUtf8(PyUnicode_AsUTF8(pyDescriptor->pyName));
}
QString description;
if (pyDescriptor->pyDescription) {
description = QString::fromUtf8(PyUnicode_AsUTF8(pyDescriptor->pyDescription));
}
ThingDescriptor descriptor(thingClassId, name, description);
descriptors.append(descriptor);
}
PythonIntegrationPlugin *plugin = s_plugins.key(self);
QMetaObject::invokeMethod(plugin, "autoThingsAppeared", Qt::QueuedConnection, Q_ARG(ThingDescriptors, descriptors));
Py_RETURN_NONE;
}
static PyMethodDef plugin_methods[] =
{
{"configuration", PythonIntegrationPlugin::pyConfiguration, METH_VARARGS, "Get the plugin configuration."},
{"configValue", PythonIntegrationPlugin::pyConfigValue, METH_VARARGS, "Get the plugin configuration value for a given config paramTypeId."},
{"setConfigValue", PythonIntegrationPlugin::pySetConfigValue, METH_VARARGS, "Set the plugin configuration value for a given config paramTypeId."},
{"myThings", PythonIntegrationPlugin::pyMyThings, METH_VARARGS, "Obtain a list of things owned by this plugin."},
{"autoThingsAppeared", PythonIntegrationPlugin::pyAutoThingsAppeared, METH_VARARGS, "Inform the system about auto setup things having appeared."},
{nullptr, nullptr, 0, nullptr} // sentinel
};
PythonIntegrationPlugin::PythonIntegrationPlugin(QObject *parent) : IntegrationPlugin(parent)
{
@ -113,8 +257,9 @@ PythonIntegrationPlugin::PythonIntegrationPlugin(QObject *parent) : IntegrationP
PythonIntegrationPlugin::~PythonIntegrationPlugin()
{
m_eventLoop.cancel();
m_eventLoop.waitForFinished();
PyGILState_STATE s = PyGILState_Ensure();
Py_XDECREF(s_plugins.take(this));
PyGILState_Release(s);
}
void PythonIntegrationPlugin::initPython()
@ -148,10 +293,11 @@ bool PythonIntegrationPlugin::loadScript(const QString &scriptFile)
qCWarning(dcThingManager()) << "Error parsing metadata file:" << error.errorString();
return false;
}
m_metaData = PluginMetadata(jsonDoc.object());
qWarning() << "main thread" << QThread::currentThread();
setMetaData(PluginMetadata(jsonDoc.object()));
if (!metadata().isValid()) {
qCWarning(dcThingManager()) << "Plugin metadata not valid for plugin:" << scriptFile;
return false;
}
PyGILState_STATE s = PyGILState_Ensure();
@ -168,10 +314,11 @@ bool PythonIntegrationPlugin::loadScript(const QString &scriptFile)
}
qCDebug(dcThingManager()) << "Imported python plugin from" << fi.absoluteFilePath();
s_plugins.insert(this, m_module);
// Set up logger with appropriate logging category
PyNymeaLoggingHandler *logger = reinterpret_cast<PyNymeaLoggingHandler*>(_PyObject_New(&PyNymeaLoggingHandlerType));
QString category = m_metaData.pluginName();
QString category = metadata().pluginName();
category.replace(0, 1, category[0].toUpper());
logger->category = static_cast<char*>(malloc(category.length() + 1));
memset(logger->category, '0', category.length() +1);
@ -182,25 +329,55 @@ bool PythonIntegrationPlugin::loadScript(const QString &scriptFile)
// Export metadata ids into module
exportIds();
// Register config access methods
PyModule_AddFunctions(m_module, plugin_methods);
PyGILState_Release(s);
// Set up connections to be forwareded into the plugin
connect(this, &PythonIntegrationPlugin::configValueChanged, this, [this](const ParamTypeId &paramTypeId, const QVariant &value){
// Sync changed value to the thread-safe copy
m_mutex.lock();
m_pluginConfigCopy.setParamValue(paramTypeId, value);
m_mutex.unlock();
// And call the handler - if any
PyObject *pyParamTypeId = PyUnicode_FromString(paramTypeId.toString().toUtf8());
PyObject *pyValue = QVariantToPyObject(value);
callPluginFunction("configValueChanged", pyParamTypeId, pyValue);
});
return true;
}
void PythonIntegrationPlugin::init()
{
callPluginFunction("init", nullptr);
m_mutex.lock();
m_pluginConfigCopy = configuration();
m_mutex.unlock();
callPluginFunction("init");
}
void PythonIntegrationPlugin::startMonitoringAutoThings()
{
callPluginFunction("startMonitoringAutoThings");
}
void PythonIntegrationPlugin::discoverThings(ThingDiscoveryInfo *info)
{
PyThingDiscoveryInfo *pyInfo = PyObject_New(PyThingDiscoveryInfo, &PyThingDiscoveryInfoType);
PyGILState_STATE s = PyGILState_Ensure();
PyThingDiscoveryInfo *pyInfo = (PyThingDiscoveryInfo*)PyObject_CallObject((PyObject*)&PyThingDiscoveryInfoType, NULL);
pyInfo->info = info;
PyGILState_Release(s);
connect(info, &ThingDiscoveryInfo::destroyed, this, [=](){
PyGILState_STATE s = PyGILState_Ensure();
QMutexLocker(pyInfo->mutex);
pyInfo->info = nullptr;
PyObject_Del(pyInfo);
PyGILState_Release(s);
Py_DECREF(pyInfo);
});
callPluginFunction("discoverThings", reinterpret_cast<PyObject*>(pyInfo));
@ -209,9 +386,9 @@ void PythonIntegrationPlugin::discoverThings(ThingDiscoveryInfo *info)
void PythonIntegrationPlugin::setupThing(ThingSetupInfo *info)
{
PyGILState_STATE s = PyGILState_Ensure();
PyThing *pyThing = (PyThing*)PyObject_CallObject((PyObject*)&PyThingType, NULL);
pyThing->thing = info->thing();
PyThing *pyThing = (PyThing*)PyObject_CallObject((PyObject*)&PyThingType, NULL);
PyThing_setThing(pyThing, info->thing());
PyThingSetupInfo *pyInfo = (PyThingSetupInfo*)PyObject_CallObject((PyObject*)&PyThingSetupInfoType, NULL);
pyInfo->info = info;
@ -222,13 +399,15 @@ void PythonIntegrationPlugin::setupThing(ThingSetupInfo *info)
connect(info, &ThingSetupInfo::finished, this, [=](){
if (info->status() == Thing::ThingErrorNoError) {
m_mutex.lock();
m_things.insert(info->thing(), pyThing);
m_mutex.unlock();
} else {
Py_DECREF(pyThing);
cleanupPyThing(pyThing);
}
});
connect(info, &ThingSetupInfo::aborted, this, [=](){
Py_DECREF(pyThing);
cleanupPyThing(pyThing);
});
connect(info, &ThingSetupInfo::destroyed, this, [=](){
QMutexLocker(pyInfo->mutex);
@ -252,21 +431,20 @@ void PythonIntegrationPlugin::executeAction(ThingActionInfo *info)
PyGILState_STATE s = PyGILState_Ensure();
PyThingActionInfo *pyInfo = PyObject_New(PyThingActionInfo, &PyThingActionInfoType);
PyThingActionInfo *pyInfo = (PyThingActionInfo*)PyObject_CallObject((PyObject*)&PyThingActionInfoType, NULL);
pyInfo->info = info;
pyInfo->pyThing = pyThing;
pyInfo->pyActionTypeId = PyUnicode_FromString(info->action().actionTypeId().toString().toUtf8());
pyInfo->pyParams = PyParam_FromParamList(info->action().params());
pyInfo->pyParams = PyParams_FromParamList(info->action().params());
PyGILState_Release(s);
connect(info, &ThingActionInfo::destroyed, this, [=](){
PyGILState_STATE s = PyGILState_Ensure();
connect(info, &ThingActionInfo::destroyed, this, [=](){
QMutexLocker(pyInfo->mutex);
pyInfo->pyActionTypeId = nullptr;
Py_XDECREF(pyInfo->pyActionTypeId);
pyInfo->info = nullptr;
Py_DECREF(pyInfo);
PyGILState_Release(s);
});
callPluginFunction("executeAction", reinterpret_cast<PyObject*>(pyInfo));
@ -278,11 +456,11 @@ void PythonIntegrationPlugin::thingRemoved(Thing *thing)
callPluginFunction("thingRemoved", reinterpret_cast<PyObject*>(pyThing));
QMutexLocker(pyThing->mutex);
pyThing->thing = nullptr;
Py_DECREF(pyThing);
cleanupPyThing(pyThing);
m_mutex.lock();
m_things.remove(thing);
m_mutex.unlock();
}
void PythonIntegrationPlugin::dumpError()
@ -309,10 +487,17 @@ void PythonIntegrationPlugin::dumpError()
void PythonIntegrationPlugin::exportIds()
{
qCDebug(dcThingManager()) << "Exporting plugin IDs:";
QString pluginName = "pluginId";
QString pluginId = m_metaData.pluginId().toString();
QString pluginName = metadata().pluginName();
QString pluginId = metadata().pluginId().toString();
qCDebug(dcThingManager()) << "- Plugin:" << pluginName << pluginId;
PyModule_AddStringConstant(m_module, pluginName.toUtf8(), pluginId.toUtf8());
PyModule_AddStringConstant(m_module, "pluginId", pluginId.toUtf8());
exportParamTypes(configurationDescription(), pluginName, "", "plugin");
foreach (const Vendor &vendor, supportedVendors()) {
qCDebug(dcThingManager()) << "|- Vendor:" << vendor.name() << vendor.id().toString();
PyModule_AddStringConstant(m_module, vendor.name().toUtf8(), vendor.id().toString().toUtf8());
}
foreach (const ThingClass &thingClass, supportedThings()) {
exportThingClass(thingClass);
@ -322,13 +507,8 @@ void PythonIntegrationPlugin::exportIds()
void PythonIntegrationPlugin::exportThingClass(const ThingClass &thingClass)
{
QString variableName = QString("%1ThingClassId").arg(thingClass.name());
if (m_variableNames.contains(variableName)) {
qWarning().nospace() << "Error: Duplicate name " << variableName << " for ThingClass " << thingClass.id() << ". Skipping entry.";
return;
}
m_variableNames.append(variableName);
qCDebug(dcThingManager()) << "|- ThingClass:" << variableName << thingClass.id();
qCDebug(dcThingManager()) << "|- ThingClass:" << variableName << thingClass.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), thingClass.id().toString().toUtf8());
exportParamTypes(thingClass.paramTypes(), thingClass.name(), "", "thing");
@ -345,12 +525,7 @@ void PythonIntegrationPlugin::exportParamTypes(const ParamTypes &paramTypes, con
{
foreach (const ParamType &paramType, paramTypes) {
QString variableName = QString("%1ParamTypeId").arg(thingClassName + typeName[0].toUpper() + typeName.right(typeName.length()-1) + typeClass + paramType.name()[0].toUpper() + paramType.name().right(paramType.name().length() -1 ));
if (m_variableNames.contains(variableName)) {
qWarning().nospace() << "Error: Duplicate name " << variableName << " for ParamTypeId " << paramType.id() << ". Skipping entry.";
continue;
}
m_variableNames.append(variableName);
qCDebug(dcThingManager()) << " |- ParamType:" << variableName << paramType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), paramType.id().toString().toUtf8());
}
}
@ -359,12 +534,7 @@ void PythonIntegrationPlugin::exportStateTypes(const StateTypes &stateTypes, con
{
foreach (const StateType &stateType, stateTypes) {
QString variableName = QString("%1%2StateTypeId").arg(thingClassName, stateType.name()[0].toUpper() + stateType.name().right(stateType.name().length() - 1));
if (m_variableNames.contains(variableName)) {
qWarning().nospace() << "Error: Duplicate name " << variableName << " for StateType " << stateType.name() << " in ThingClass " << thingClassName << ". Skipping entry.";
return;
}
m_variableNames.append(variableName);
qCDebug(dcThingManager()) << "|- StateType:" << variableName << stateType.id();
qCDebug(dcThingManager()) << " |- StateType:" << variableName << stateType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), stateType.id().toString().toUtf8());
}
}
@ -373,13 +543,8 @@ void PythonIntegrationPlugin::exportEventTypes(const EventTypes &eventTypes, con
{
foreach (const EventType &eventType, eventTypes) {
QString variableName = QString("%1%2EventTypeId").arg(thingClassName, eventType.name()[0].toUpper() + eventType.name().right(eventType.name().length() - 1));
if (m_variableNames.contains(variableName)) {
qWarning().nospace() << "Error: Duplicate name " << variableName << " for EventType " << eventType.name() << " in ThingClass " << thingClassName << ". Skipping entry.";
return;
}
m_variableNames.append(variableName);
qCDebug(dcThingManager()) << " |- EventType:" << variableName << eventType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), eventType.id().toString().toUtf8());
exportParamTypes(eventType.paramTypes(), thingClassName, "Event", eventType.name());
}
@ -389,13 +554,8 @@ void PythonIntegrationPlugin::exportActionTypes(const ActionTypes &actionTypes,
{
foreach (const ActionType &actionType, actionTypes) {
QString variableName = QString("%1%2ActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1));
if (m_variableNames.contains(variableName)) {
qWarning().nospace() << "Error: Duplicate name " << variableName << " for ActionType " << actionType.name() << " in ThingClass " << thingClassName << ". Skipping entry.";
return;
}
m_variableNames.append(variableName);
qCDebug(dcThingManager()) << " |- ActionType:" << variableName << actionType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), actionType.id().toString().toUtf8());
exportParamTypes(actionType.paramTypes(), thingClassName, "Action", actionType.name());
}
}
@ -404,20 +564,15 @@ void PythonIntegrationPlugin::exportBrowserItemActionTypes(const ActionTypes &ac
{
foreach (const ActionType &actionType, actionTypes) {
QString variableName = QString("%1%2BrowserItemActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1));
if (m_variableNames.contains(variableName)) {
qWarning().nospace() << "Error: Duplicate name " << variableName << " for Browser Item ActionType " << actionType.name() << " in ThingClass " << thingClassName << ". Skipping entry.";
return;
}
m_variableNames.append(variableName);
qCDebug(dcThingManager()) << " |- BrowserActionType:" << variableName << actionType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), actionType.id().toString().toUtf8());
exportParamTypes(actionType.paramTypes(), thingClassName, "BrowserItemAction", actionType.name());
}
}
void PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObject *param)
void PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObject *param1, PyObject *param2)
{
PyGILState_STATE s = PyGILState_Ensure();
@ -433,7 +588,7 @@ void PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObje
dumpError();
PyObject *result = PyObject_CallFunctionObjArgs(pFunc, param, nullptr);
PyObject *result = PyObject_CallFunctionObjArgs(pFunc, param1, param2, nullptr);
Py_XDECREF(pFunc);
@ -485,3 +640,18 @@ void PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObje
PyGILState_Release(s);
}
void PythonIntegrationPlugin::cleanupPyThing(PyThing *pyThing)
{
// It could happen that the python thread is currently holding the mutex
// whike waiting on a blocking queued connection on the thing (e.g. PyThing_name).
// We'd deadlock if we wait for the mutex forever here. So let's process events
// while waiting for it...
while (!pyThing->mutex->tryLock()) {
qApp->processEvents(QEventLoop::EventLoopExec);
}
pyThing->thing = nullptr;
pyThing->mutex->unlock();
Py_DECREF(pyThing);
}

View File

@ -26,6 +26,7 @@ public:
bool loadScript(const QString &scriptFile);
void init() override;
void startMonitoringAutoThings() override;
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
@ -34,6 +35,12 @@ public:
static void dumpError();
static PyObject* pyConfiguration(PyObject* self, PyObject* args);
static PyObject* pyConfigValue(PyObject* self, PyObject* args);
static PyObject* pySetConfigValue(PyObject* self, PyObject* args);
static PyObject* pyMyThings(PyObject *self, PyObject* args);
static PyObject* pyAutoThingsAppeared(PyObject *self, PyObject* args);
private:
void exportIds();
void exportThingClass(const ThingClass &thingClass);
@ -44,22 +51,31 @@ private:
void exportBrowserItemActionTypes(const ActionTypes &actionTypes, const QString &thingClassName);
void callPluginFunction(const QString &function, PyObject *param);
void callPluginFunction(const QString &function, PyObject *param1 = nullptr, PyObject *param2 = nullptr);
void cleanupPyThing(PyThing *pyThing);
private:
// static QHash<PyObject*, PyThreadState*> s_modules;
static PyThreadState* s_mainThread;
// Modules imported into the global context
static PyObject *s_nymeaModule;
static PyObject *s_asyncio;
PyObject *m_module = nullptr;
QFuture<void> m_eventLoop;
// A map of plugin instances to plugin python scripts/modules
// Make sure to hold the GIL when accessing this.
static QHash<PythonIntegrationPlugin*, PyObject*> s_plugins;
// Used for guarding access from the python threads to the plugin instance
QMutex m_mutex;
// The imported plugin module
PyObject *m_module = nullptr;
// Things held by this plugin instance
QHash<Thing*, PyThing*> m_things;
QStringList m_variableNames;
// Need to keep a copy of plugin params and sync that in a thread-safe manner
ParamList m_pluginConfigCopy;
};
#endif // PYTHONINTEGRATIONPLUGIN_H

View File

@ -95,6 +95,8 @@ ThingManagerImplementation::ThingManagerImplementation(HardwareManager *hardware
// Make sure this is always emitted after plugins and things are loaded
QMetaObject::invokeMethod(this, "onLoaded", Qt::QueuedConnection);
PythonIntegrationPlugin::initPython();
}
ThingManagerImplementation::~ThingManagerImplementation()
@ -159,13 +161,13 @@ QList<QJsonObject> ThingManagerImplementation::pluginsMetadata()
return pluginList;
}
void ThingManagerImplementation::registerStaticPlugin(IntegrationPlugin *plugin, const PluginMetadata &metaData)
void ThingManagerImplementation::registerStaticPlugin(IntegrationPlugin *plugin)
{
if (!metaData.isValid()) {
if (!plugin->metadata().isValid()) {
qCWarning(dcThingManager()) << "Plugin metadata not valid. Not loading static plugin:" << plugin->pluginName();
return;
}
loadPlugin(plugin, metaData);
loadPlugin(plugin);
}
IntegrationPlugins ThingManagerImplementation::plugins() const
@ -1230,161 +1232,73 @@ ThingActionInfo *ThingManagerImplementation::executeAction(const Action &action)
}
void ThingManagerImplementation::loadPlugins()
{
{
QStringList searchDirs;
// Add first level of subdirectories to the plugin search dirs so we can point to a collection of plugins
foreach (const QString &path, pluginSearchDirs()) {
searchDirs.append(path);
QDir dir(path);
foreach (const QString &subdir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
searchDirs.append(path + '/' + subdir);
}
}
foreach (const QString &path, searchDirs) {
QDir dir(path);
qCDebug(dcThingManager) << "Loading plugins from:" << dir.absolutePath();
foreach (const QString &entry, dir.entryList()) {
QFileInfo fi;
foreach (const QString &entry, dir.entryList({"*.so", "*.js", "*.py"}, QDir::Files)) {
IntegrationPlugin *plugin = nullptr;
QFileInfo fi(path + '/' + entry);
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<QString(*)()>(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<IntegrationPlugin *>(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);
}
}
plugin = createCppIntegrationPlugin(fi.absoluteFilePath());
} else if (entry.startsWith("integrationplugin") && entry.endsWith(".js")) {
#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;
delete plugin;
continue;
ScriptIntegrationPlugin *p = new ScriptIntegrationPlugin(this);
bool ok = p->loadScript(fi.absoluteFilePath());
if (ok) {
plugin = p;
} else {
delete p;
}
}
loadPlugin(plugin, metaData);
}
}
#else
qCWarning(dcThingManager()) << "Not loading JS plugin as JS plugin support is not included in this nymea instance."
#endif
PythonIntegrationPlugin::initPython();
{
PythonIntegrationPlugin *p = new PythonIntegrationPlugin(this);
bool ok = p->loadScript("/home/micha/Develop/nymea-plugin-pytest/integrationpluginpytest.py");
if (!ok) {
qCWarning(dcThingManager()) << "Error loading plugin";
return;
}
if (!p->metadata().isValid()) {
qCWarning(dcThingManager()) << "Not loading Python plugin. Invalid metadata.";
foreach (const QString &error, p->metadata().validationErrors()) {
qCWarning(dcThingManager()) << error;
} else if (entry.startsWith("integrationplugin") && entry.endsWith(".py")) {
PythonIntegrationPlugin *p = new PythonIntegrationPlugin(this);
bool ok = p->loadScript(fi.absoluteFilePath());
if (ok) {
plugin = p;
} else {
delete p;
}
} else {
// Not a known plugin type
continue;
}
return;
}
loadPlugin(p, p->metadata());
}
{
PythonIntegrationPlugin *p = new PythonIntegrationPlugin(this);
bool ok = p->loadScript("/home/micha/Develop/nymea-plugin-pytest2/integrationpluginpytest2.py");
if (!ok) {
qCWarning(dcThingManager()) << "Error loading plugin";
return;
}
PluginMetadata metaData(p->metadata());
if (!metaData.isValid()) {
qCWarning(dcThingManager()) << "Not loading Python plugin. Invalid metadata.";
foreach (const QString &error, metaData.validationErrors()) {
qCWarning(dcThingManager()) << error;
}
return;
}
loadPlugin(p, metaData);
if (!plugin) {
qCWarning(dcThingManager()) << "Error loading plugin:" << fi.absoluteFilePath();
continue;
}
if (m_integrationPlugins.contains(plugin->pluginId())) {
qCWarning(dcThingManager()) << "A plugin with this ID is already loaded. Not loading" << entry << plugin->pluginId();
delete plugin;
continue;
}
loadPlugin(plugin);
PluginInfoCache::cachePluginInfo(plugin->metadata().jsonObject());
}
}
}
void ThingManagerImplementation::loadPlugin(IntegrationPlugin *pluginIface, const PluginMetadata &metaData)
void ThingManagerImplementation::loadPlugin(IntegrationPlugin *pluginIface)
{
pluginIface->setParent(this);
pluginIface->initPlugin(metaData, this, m_hardwareManager);
pluginIface->initPlugin(this, m_hardwareManager);
qCDebug(dcThingManager) << "**** Loaded plugin" << pluginIface->pluginName();
foreach (const Vendor &vendor, pluginIface->supportedVendors()) {
@ -2152,6 +2066,68 @@ void ThingManagerImplementation::registerThing(Thing *thing)
connect(thing, &Thing::nameChanged, this, &ThingManagerImplementation::slotThingNameChanged);
}
IntegrationPlugin *ThingManagerImplementation::createCppIntegrationPlugin(const QString &absoluteFilePath)
{
// Check plugin API version compatibility
QLibrary lib(absoluteFilePath);
if (!lib.load()) {
qCWarning(dcThingManager()).nospace() << "Error loading plugin " << absoluteFilePath << ": " << lib.errorString();
return nullptr;
}
QFunctionPointer versionFunc = lib.resolve("libnymea_api_version");
if (!versionFunc) {
qCWarning(dcThingManager()).nospace() << "Unable to resolve version in plugin " << absoluteFilePath << ". Not loading plugin.";
lib.unload();
return nullptr;
}
QString version = reinterpret_cast<QString(*)()>(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 " << absoluteFilePath << ". Core API: " << LIBNYMEA_API_VERSION << ", Plugin API: " << version;
return nullptr;
}
// Version is ok. Now load the plugin
QPluginLoader loader;
loader.setFileName(absoluteFilePath);
loader.setLoadHints(QLibrary::ResolveAllSymbolsHint);
qCDebug(dcThingManager()) << "Loading plugin from:" << absoluteFilePath;
if (!loader.load()) {
qCWarning(dcThingManager) << "Could not load plugin data of" << absoluteFilePath << "\n" << loader.errorString();
return nullptr;
}
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();
return nullptr;
}
QObject *p = loader.instance();
if (!p) {
qCWarning(dcThingManager()) << "Error loading plugin:" << loader.errorString();
return nullptr;
}
IntegrationPlugin *pluginIface = qobject_cast<IntegrationPlugin *>(p);
if (!pluginIface) {
qCWarning(dcThingManager) << "Could not get plugin instance of" << absoluteFilePath;
return nullptr;
}
pluginIface->setMetaData(PluginMetadata(pluginInfo));
return pluginIface;
}
void ThingManagerImplementation::storeThingStates(Thing *thing)
{
ThingClass thingClass = m_supportedThings.value(thing->thingClassId());

View File

@ -72,7 +72,7 @@ public:
static QStringList pluginSearchDirs();
static QList<QJsonObject> pluginsMetadata();
void registerStaticPlugin(IntegrationPlugin* plugin, const PluginMetadata &metaData);
void registerStaticPlugin(IntegrationPlugin* plugin);
IntegrationPlugins plugins() const override;
IntegrationPlugin *plugin(const PluginId &pluginId) const override;
@ -131,7 +131,7 @@ signals:
private slots:
void loadPlugins();
void loadPlugin(IntegrationPlugin *pluginIface, const PluginMetadata &metaData);
void loadPlugin(IntegrationPlugin *pluginIface);
void loadConfiguredThings();
void storeConfiguredThings();
void startMonitoringAutoThings();
@ -165,6 +165,8 @@ private:
void syncIOConnection(Thing *inputThing, const StateTypeId &stateTypeId);
QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const;
IntegrationPlugin *createCppIntegrationPlugin(const QString &absoluteFilePath);
private:
HardwareManager *m_hardwareManager;

View File

@ -27,6 +27,7 @@ HEADERS += nymeacore.h \
integrations/python/pythingdescriptor.h \
integrations/python/pythingdiscoveryinfo.h \
integrations/python/pythingsetupinfo.h \
integrations/python/pyutils.h \
integrations/thingmanagerimplementation.h \
integrations/translator.h \
integrations/pythonintegrationplugin.h \

View File

@ -137,7 +137,7 @@ void NymeaCore::init() {
CloudNotifications *cloudNotifications = m_cloudManager->createNotificationsPlugin();
m_thingManager->registerStaticPlugin(cloudNotifications, cloudNotifications->metaData());
m_thingManager->registerStaticPlugin(cloudNotifications);
CloudTransport *cloudTransport = m_cloudManager->createTransportInterface();
m_serverManager->jsonServer()->registerTransportInterface(cloudTransport, false);

View File

@ -374,9 +374,8 @@ ParamTypes IntegrationPlugin::configurationDescription() const
return m_metaData.pluginSettings();
}
void IntegrationPlugin::initPlugin(const PluginMetadata &metadata, ThingManager *thingManager, HardwareManager *hardwareManager)
void IntegrationPlugin::initPlugin(ThingManager *thingManager, HardwareManager *hardwareManager)
{
m_metaData = metadata;
m_thingManager = thingManager;
m_hardwareManager = hardwareManager;
m_storage = new QSettings(NymeaSettings::settingsPath() + "/pluginconfig-" + pluginId().toString().remove(QRegExp("[{}]")) + ".conf", QSettings::IniFormat, this);

View File

@ -107,11 +107,11 @@ public:
virtual void executeBrowserItemAction(BrowserItemActionInfo *info);
// Configuration
ParamTypes configurationDescription() const;
Thing::ThingError setConfiguration(const ParamList &configuration);
ParamList configuration() const;
QVariant configValue(const ParamTypeId &paramTypeId) const;
Thing::ThingError setConfigValue(const ParamTypeId &paramTypeId, const QVariant &value);
Q_INVOKABLE ParamTypes configurationDescription() const;
Q_INVOKABLE Thing::ThingError setConfiguration(const ParamList &configuration);
Q_INVOKABLE ParamList configuration() const;
Q_INVOKABLE QVariant configValue(const ParamTypeId &paramTypeId) const;
Q_INVOKABLE Thing::ThingError setConfigValue(const ParamTypeId &paramTypeId, const QVariant &value);
bool isBuiltIn() const;
@ -126,25 +126,15 @@ protected:
HardwareManager *hardwareManager() const;
QSettings *pluginStorage() const;
PluginMetadata m_metaData;
void setMetaData(const PluginMetadata &metaData);
private:
friend class ThingManager;
friend class ThingManagerImplementation;
void initPlugin(ThingManager *thingManager, HardwareManager *hardwareManager);
void setMetaData(const PluginMetadata &metaData);
void initPlugin(const PluginMetadata &metadata, ThingManager *thingManager, HardwareManager *hardwareManager);
QPair<bool, QList<ParamType> > parseParamTypes(const QJsonArray &array) const;
// Returns <missingFields, unknownFields>
QPair<QStringList, QStringList> verifyFields(const QStringList &possibleFields, const QStringList &mandatoryFields, const QJsonObject &value) const;
// load and verify enum values
QPair<bool, Types::Unit> loadAndVerifyUnit(const QString &unitString) const;
QPair<bool, Types::InputType> loadAndVerifyInputType(const QString &inputType) const;
PluginMetadata m_metaData;
ThingManager *m_thingManager = nullptr;
HardwareManager *m_hardwareManager = nullptr;
QSettings *m_storage = nullptr;

View File

@ -47,6 +47,7 @@ PluginMetadata::PluginMetadata()
}
PluginMetadata::PluginMetadata(const QJsonObject &jsonObject, bool isBuiltIn, bool strict):
m_jsonObject(jsonObject),
m_isBuiltIn(isBuiltIn),
m_strictRun(strict)
{
@ -98,6 +99,11 @@ ThingClasses PluginMetadata::thingClasses() const
return m_thingClasses;
}
QJsonObject PluginMetadata::jsonObject() const
{
return m_jsonObject;
}
void PluginMetadata::parse(const QJsonObject &jsonObject)
{
bool hasError = false;

View File

@ -34,6 +34,8 @@
#include "types/paramtype.h"
#include "types/thingclass.h"
#include <QJsonObject>
class PluginMetadata
{
public:
@ -53,6 +55,7 @@ public:
Vendors vendors() const;
ThingClasses thingClasses() const;
QJsonObject jsonObject() const;
private:
void parse(const QJsonObject &jsonObject);
@ -64,6 +67,7 @@ private:
bool verifyDuplicateUuid(const QUuid &uuid);
private:
QJsonObject m_jsonObject;
bool m_isValid = false;
bool m_isBuiltIn = false;
PluginId m_pluginId;

View File

@ -7,7 +7,7 @@ NYMEA_VERSION_STRING=$$system('dpkg-parsechangelog | sed -n -e "s/^Version: //p"
JSON_PROTOCOL_VERSION_MAJOR=5
JSON_PROTOCOL_VERSION_MINOR=1
JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}"
LIBNYMEA_API_VERSION_MAJOR=6
LIBNYMEA_API_VERSION_MAJOR=7
LIBNYMEA_API_VERSION_MINOR=0
LIBNYMEA_API_VERSION_PATCH=0
LIBNYMEA_API_VERSION="$${LIBNYMEA_API_VERSION_MAJOR}.$${LIBNYMEA_API_VERSION_MINOR}.$${LIBNYMEA_API_VERSION_PATCH}"

View File

@ -4,7 +4,7 @@ QT+= network
TARGET = $$qtLibraryTarget(nymea_integrationpluginmock)
OTHER_FILES += interationpluginmock.json
OTHER_FILES += integrationpluginmock.json
SOURCES += \
integrationpluginmock.cpp \

View File

@ -9,7 +9,7 @@
#include <QLoggingCategory>
#include <QObject>
extern "C" const QString libnymea_api_version() { return QString("6.0.0");}
extern "C" const QString libnymea_api_version() { return QString("7.0.0");}
Q_DECLARE_LOGGING_CATEGORY(dcMock)
Q_LOGGING_CATEGORY(dcMock, "Mock")

View File

@ -1,6 +1,6 @@
TEMPLATE = subdirs
!disabletesting: {
SUBDIRS += mock
SUBDIRS += mock pymock
}

4
plugins/pymock/ids.py Normal file
View File

@ -0,0 +1,4 @@
import builtins
builtins.pyMockPluginAutoThingCountParamTypeId = "{1d3422cb-fcdd-4ab5-ac6e-056288439343}"
builtins.foo = "bar"

View File

@ -0,0 +1,76 @@
{
"id": "9be90b21-778c-4080-93c9-84ae1ab60734",
"name": "pyMock",
"displayName": "Python mock plugin",
"paramTypes": [
{
"id": "1d3422cb-fcdd-4ab5-ac6e-056288439343",
"name": "autoThingCount",
"displayName": "Number of auto things",
"type": "int",
"defaultValue": 0
}
],
"vendors": [
{
"id": "2062d64d-3232-433c-88bc-0d33c0ba2ba6",
"name": "nymea",
"displayName": "nymea GmbH",
"thingClasses": [
{
"id": "727d3e73-505d-4118-adf4-6408b46a5d48",
"name": "pyMockAuto",
"displayName": "Auto Python mock thing",
"createMethods": ["auto"],
"setupMethod": "justAdd",
"stateTypes": [
{
"id": "12c82472-56b0-4229-b324-4aaa6850320e",
"name": "state1",
"displayName": "State 1",
"displayNameEvent": "State 1 changed",
"type": "bool",
"defaultValue": false
}
]
},
{
"id": "1761c256-99b1-41bd-988a-a76087f6a4f1",
"name": "pyMock",
"displayName": "Python mock thing",
"createMethods": ["user"],
"setupMethod": "justAdd",
"stateTypes": [
{
"id": "24714828-93ec-41a7-875e-6a0b5b57d25c",
"name": "state1",
"displayName": "State 1",
"displayNameEvent": "State 1 changed",
"displayNameAction": "Set state 1",
"type": "int",
"defaultValue": 0
}
]
},
{
"id": "248c5046-847b-44d0-ab7c-684ff79197dc",
"name": "pyMockDiscoveryPairing",
"displayName": "Python mock thing with discovery and pairing",
"createMethods": ["discovery"],
"setupMethod": "userAndPassword",
"stateTypes": [
{
"id": "99d0af17-9e8c-42bb-bece-a5d114f051d3",
"name": "state1",
"displayName": "State 1",
"displayNameEvent": "State 1 changed",
"displayNameAction": "Set state 1",
"type": "int",
"defaultValue": 0
}
]
}
]
}
]
}

View File

@ -0,0 +1,17 @@
import nymea
def init():
logger.log("Python mock plugin init")
logger.log("Number of auto mocks", configValue(pyMockPluginAutoThingCountParamTypeId))
def configValueChanged(paramTypeId, value):
logger.log("Plugin config value changed:", paramTypeId, value)
def startMonitoringAutoThings():
logger.log("Start monitoring auto things. Already have", len(myThings()))
for i in range(configValue(pyMockPluginAutoThingCountParamTypeId), len(myThings())):
logger.log("auto thing")
# descriptor = nymea.
# autoThingsAppeared(

22
plugins/pymock/pymock.pro Normal file
View File

@ -0,0 +1,22 @@
TEMPLATE = aux
OTHER_FILES = integrationpluginpymock.json \
integrationpluginpymock.py
# Copy files to build dir as we've set plugin import paths to that
#mytarget.target = integrationpluginpymock.py
#mytarget.commands = cp $$PWD/$$mytarget.target $$mytarget.target
#mytarget.depends = mytarget2
#mytarget2.commands = cp $$PWD/integrationpluginpymock.json integrationpluginpymock.json
#QMAKE_EXTRA_TARGETS += mytarget mytarget2
copydata.commands = $(COPY_DIR) $$PWD/integrationpluginpymock.json $$PWD/*.py $$OUT_PWD
first.depends = $(first) copydata
export(first.depends)
export(copydata.commands)
QMAKE_EXTRA_TARGETS += first copydata