diff --git a/libnymea-core/cloud/cloudnotifications.cpp b/libnymea-core/cloud/cloudnotifications.cpp index 38a426e5..678fc4e1 100644 --- a/libnymea-core/cloud/cloudnotifications.cpp +++ b/libnymea-core/cloud/cloudnotifications.cpp @@ -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) diff --git a/libnymea-core/integrations/python/pyparam.h b/libnymea-core/integrations/python/pyparam.h index db6da3bd..cd84a4c2 100644 --- a/libnymea-core/integrations/python/pyparam.h +++ b/libnymea-core/integrations/python/pyparam.h @@ -2,7 +2,9 @@ #define PYPARAM_H #include -#include "structmember.h" +#include + +#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 ¶m) { 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 ¶ms) +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 ¶ms) { PyObject* result = PyTuple_New(params.count()); for (int i = 0; i < params.count(); i++) { @@ -128,6 +108,31 @@ static PyObject* PyParam_FromParamList(const ParamList ¶ms) 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(next); + params.append(PyParam_ToParam(pyParam)); + } + return params; +} static void registerParamType(PyObject *module) { diff --git a/libnymea-core/integrations/python/pything.h b/libnymea-core/integrations/python/pything.h index b396bc5d..0c742258 100644 --- a/libnymea-core/integrations/python/pything.h +++ b/libnymea-core/integrations/python/pything.h @@ -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(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 */ diff --git a/libnymea-core/integrations/python/pythingactioninfo.h b/libnymea-core/integrations/python/pythingactioninfo.h index 08dc1898..243dc9f6 100644 --- a/libnymea-core/integrations/python/pythingactioninfo.h +++ b/libnymea-core/integrations/python/pythingactioninfo.h @@ -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; } diff --git a/libnymea-core/integrations/python/pythingdescriptor.h b/libnymea-core/integrations/python/pythingdescriptor.h index a57660b8..777d5b3f 100644 --- a/libnymea-core/integrations/python/pythingdescriptor.h +++ b/libnymea-core/integrations/python/pythingdescriptor.h @@ -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(PyThingDescriptor_init); diff --git a/libnymea-core/integrations/python/pythingdiscoveryinfo.h b/libnymea-core/integrations/python/pythingdiscoveryinfo.h index 7037aaa6..af20d574 100644 --- a/libnymea-core/integrations/python/pythingdiscoveryinfo.h +++ b/libnymea-core/integrations/python/pythingdiscoveryinfo.h @@ -11,6 +11,7 @@ #include #include +#include #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(PyThingDiscoveryInfo_dealloc); - PyThingDiscoveryInfoType.tp_flags = Py_TPFLAGS_DEFAULT; - PyThingDiscoveryInfoType.tp_doc = "ThingDiscoveryInfo class"; - PyThingDiscoveryInfoType.tp_methods = PyThingDiscoveryInfo_methods; - PyThingDiscoveryInfoType.tp_init = reinterpret_cast(PyThingDiscoveryInfo_init); if (PyType_Ready(&PyThingDiscoveryInfoType) < 0) { return; diff --git a/libnymea-core/integrations/python/pythingsetupinfo.h b/libnymea-core/integrations/python/pythingsetupinfo.h index fbb76d21..ab4a589f 100644 --- a/libnymea-core/integrations/python/pythingsetupinfo.h +++ b/libnymea-core/integrations/python/pythingsetupinfo.h @@ -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; } diff --git a/libnymea-core/integrations/python/pyutils.h b/libnymea-core/integrations/python/pyutils.h new file mode 100644 index 00000000..ccce2c43 --- /dev/null +++ b/libnymea-core/integrations/python/pyutils.h @@ -0,0 +1,61 @@ +#ifndef PYUTILS_H +#define PYUTILS_H + +#include + +#include "loggingcategories.h" + +#include + +/* 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 diff --git a/libnymea-core/integrations/pythonintegrationplugin.cpp b/libnymea-core/integrations/pythonintegrationplugin.cpp index a4551ed1..3703cc4b 100644 --- a/libnymea-core/integrations/pythonintegrationplugin.cpp +++ b/libnymea-core/integrations/pythonintegrationplugin.cpp @@ -15,11 +15,14 @@ #include #include #include +#include +#include PyThreadState* PythonIntegrationPlugin::s_mainThread = nullptr; PyObject* PythonIntegrationPlugin::s_nymeaModule = nullptr; PyObject* PythonIntegrationPlugin::s_asyncio = nullptr; +QHash 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", ¶mTypeIdStr)) { + 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", ¶mTypeIdStr, &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(_PyObject_New(&PyNymeaLoggingHandlerType)); - QString category = m_metaData.pluginName(); + QString category = metadata().pluginName(); category.replace(0, 1, category[0].toUpper()); logger->category = static_cast(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 ¶mTypeId, 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(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(pyInfo)); @@ -278,11 +456,11 @@ void PythonIntegrationPlugin::thingRemoved(Thing *thing) callPluginFunction("thingRemoved", reinterpret_cast(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 ¶mTypes, con { foreach (const ParamType ¶mType, 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); +} + diff --git a/libnymea-core/integrations/pythonintegrationplugin.h b/libnymea-core/integrations/pythonintegrationplugin.h index a7cc47b2..f0145fe0 100644 --- a/libnymea-core/integrations/pythonintegrationplugin.h +++ b/libnymea-core/integrations/pythonintegrationplugin.h @@ -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 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 m_eventLoop; + // A map of plugin instances to plugin python scripts/modules + // Make sure to hold the GIL when accessing this. + static QHash 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 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 diff --git a/libnymea-core/integrations/thingmanagerimplementation.cpp b/libnymea-core/integrations/thingmanagerimplementation.cpp index c8b0f14f..d1a8a8d3 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -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 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(versionFunc)(); - lib.unload(); - QStringList parts = version.split('.'); - QStringList coreParts = QString(LIBNYMEA_API_VERSION).split('.'); - if (parts.length() != 3 || parts.at(0).toInt() != coreParts.at(0).toInt() || parts.at(1).toInt() > coreParts.at(1).toInt()) { - qCWarning(dcThingManager()).nospace() << "Libnymea API mismatch for " << fi.absoluteFilePath() << ". Core API: " << LIBNYMEA_API_VERSION << ", Plugin API: " << version; - continue; - } - - // Version is ok. Now load the plugin - QPluginLoader loader; - loader.setFileName(fi.absoluteFilePath()); - loader.setLoadHints(QLibrary::ResolveAllSymbolsHint); - - qCDebug(dcThingManager()) << "Loading plugin from:" << fi.absoluteFilePath(); - if (!loader.load()) { - qCWarning(dcThingManager) << "Could not load plugin data of" << entry << "\n" << loader.errorString(); - continue; - } - - QJsonObject pluginInfo = loader.metaData().value("MetaData").toObject(); - PluginMetadata metaData(pluginInfo, false, false); - if (!metaData.isValid()) { - foreach (const QString &error, metaData.validationErrors()) { - qCWarning(dcThingManager()) << error; - } - loader.unload(); - continue; - } - - IntegrationPlugin *pluginIface = qobject_cast(loader.instance()); - if (!pluginIface) { - qCWarning(dcThingManager) << "Could not get plugin instance of" << entry; - loader.unload(); - continue; - } - if (m_integrationPlugins.contains(pluginIface->pluginId())) { - qCWarning(dcThingManager()) << "A plugin with this ID is already loaded. Not loading" << entry; - continue; - } - loadPlugin(pluginIface, metaData); - PluginInfoCache::cachePluginInfo(pluginInfo); - } - } + 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(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(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()); diff --git a/libnymea-core/integrations/thingmanagerimplementation.h b/libnymea-core/integrations/thingmanagerimplementation.h index ed6c4060..c127eef7 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.h +++ b/libnymea-core/integrations/thingmanagerimplementation.h @@ -72,7 +72,7 @@ public: static QStringList pluginSearchDirs(); static QList 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; diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index d40891df..111f18d6 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -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 \ diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index cd94309a..48c144c1 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -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); diff --git a/libnymea/integrations/integrationplugin.cpp b/libnymea/integrations/integrationplugin.cpp index 17faaf45..3f15c8c9 100644 --- a/libnymea/integrations/integrationplugin.cpp +++ b/libnymea/integrations/integrationplugin.cpp @@ -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); diff --git a/libnymea/integrations/integrationplugin.h b/libnymea/integrations/integrationplugin.h index afefb72d..daeb4b06 100644 --- a/libnymea/integrations/integrationplugin.h +++ b/libnymea/integrations/integrationplugin.h @@ -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 ¶mTypeId) const; - Thing::ThingError setConfigValue(const ParamTypeId ¶mTypeId, 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 ¶mTypeId) const; + Q_INVOKABLE Thing::ThingError setConfigValue(const ParamTypeId ¶mTypeId, 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 > parseParamTypes(const QJsonArray &array) const; - - // Returns - QPair verifyFields(const QStringList &possibleFields, const QStringList &mandatoryFields, const QJsonObject &value) const; - - // load and verify enum values - QPair loadAndVerifyUnit(const QString &unitString) const; - QPair loadAndVerifyInputType(const QString &inputType) const; - + PluginMetadata m_metaData; ThingManager *m_thingManager = nullptr; HardwareManager *m_hardwareManager = nullptr; QSettings *m_storage = nullptr; diff --git a/libnymea/integrations/pluginmetadata.cpp b/libnymea/integrations/pluginmetadata.cpp index cd71154c..391993ea 100644 --- a/libnymea/integrations/pluginmetadata.cpp +++ b/libnymea/integrations/pluginmetadata.cpp @@ -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; diff --git a/libnymea/integrations/pluginmetadata.h b/libnymea/integrations/pluginmetadata.h index e95937e2..6fb5861b 100644 --- a/libnymea/integrations/pluginmetadata.h +++ b/libnymea/integrations/pluginmetadata.h @@ -34,6 +34,8 @@ #include "types/paramtype.h" #include "types/thingclass.h" +#include + 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; diff --git a/nymea.pro b/nymea.pro index 1b2879eb..edb20dce 100644 --- a/nymea.pro +++ b/nymea.pro @@ -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}" diff --git a/plugins/mock/mock.pro b/plugins/mock/mock.pro index 975cc88e..63d494d4 100644 --- a/plugins/mock/mock.pro +++ b/plugins/mock/mock.pro @@ -4,7 +4,7 @@ QT+= network TARGET = $$qtLibraryTarget(nymea_integrationpluginmock) -OTHER_FILES += interationpluginmock.json +OTHER_FILES += integrationpluginmock.json SOURCES += \ integrationpluginmock.cpp \ diff --git a/plugins/mock/plugininfo.h b/plugins/mock/plugininfo.h index 4e4b891a..5b9e2da9 100644 --- a/plugins/mock/plugininfo.h +++ b/plugins/mock/plugininfo.h @@ -9,7 +9,7 @@ #include #include -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") diff --git a/plugins/plugins.pro b/plugins/plugins.pro index 71c20849..03817529 100644 --- a/plugins/plugins.pro +++ b/plugins/plugins.pro @@ -1,6 +1,6 @@ TEMPLATE = subdirs !disabletesting: { - SUBDIRS += mock + SUBDIRS += mock pymock } diff --git a/plugins/pymock/ids.py b/plugins/pymock/ids.py new file mode 100644 index 00000000..9941ae76 --- /dev/null +++ b/plugins/pymock/ids.py @@ -0,0 +1,4 @@ +import builtins + +builtins.pyMockPluginAutoThingCountParamTypeId = "{1d3422cb-fcdd-4ab5-ac6e-056288439343}" +builtins.foo = "bar" diff --git a/plugins/pymock/integrationpluginpymock.json b/plugins/pymock/integrationpluginpymock.json new file mode 100644 index 00000000..727a061b --- /dev/null +++ b/plugins/pymock/integrationpluginpymock.json @@ -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 + } + ] + } + ] + } + ] +} diff --git a/plugins/pymock/integrationpluginpymock.py b/plugins/pymock/integrationpluginpymock.py new file mode 100644 index 00000000..862b75de --- /dev/null +++ b/plugins/pymock/integrationpluginpymock.py @@ -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( diff --git a/plugins/pymock/pymock.pro b/plugins/pymock/pymock.pro new file mode 100644 index 00000000..bdf46f5d --- /dev/null +++ b/plugins/pymock/pymock.pro @@ -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