diff --git a/debian/control b/debian/control index 43764632..13688d65 100644 --- a/debian/control +++ b/debian/control @@ -12,6 +12,7 @@ Build-Depends: debhelper (>= 9.0.0), libnymea-mqtt-dev (>= 0.1.2), libnymea-networkmanager-dev (>= 0.4.0), libnymea-remoteproxyclient-dev, + libpython3-dev, libqt5websockets5-dev, libqt5bluetooth5, libqt5sql5-sqlite, 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/pynymealogginghandler.h b/libnymea-core/integrations/python/pynymealogginghandler.h new file mode 100644 index 00000000..fe2e8970 --- /dev/null +++ b/libnymea-core/integrations/python/pynymealogginghandler.h @@ -0,0 +1,128 @@ +#ifndef PYNYMEALOGGINGHANDLER_H +#define PYNYMEALOGGINGHANDLER_H + +#include +#include "structmember.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(dcPythonIntegrations) + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +typedef struct { + PyObject_HEAD + char *category; +} PyNymeaLoggingHandler; + +static int PyNymeaLoggingHandler_init(PyNymeaLoggingHandler *self, PyObject *args, PyObject */*kwds*/) +{ + qCDebug(dcPythonIntegrations()) << "+++ PyNymeaLoggingHandler"; + char *category = nullptr; + if (!PyArg_ParseTuple(args, "s", &category)) { + qCWarning(dcPythonIntegrations()) << "PyNymeaLoggingHandler: Error parsing parameters"; + return -1; + } + + self->category = (char*)malloc(qstrlen(category)); + qstrcpy(self->category, category); + + return 0; +} + +static void PyNymeaLoggingHandler_dealloc(PyNymeaLoggingHandler * self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyNymeaLoggingHandler"; + free(self->category); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * PyNymeaLoggingHandler_info(PyNymeaLoggingHandler* self, PyObject* args) +{ + QStringList strings; + for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { + PyObject *obj = PyTuple_GET_ITEM(args, i); + PyObject* repr = PyObject_Repr(obj); + PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + const char *bytes = PyBytes_AS_STRING(str); + Py_XDECREF(repr); + Py_XDECREF(str); + strings.append(bytes); + } + // FIXME: We'll want to use qCInfo() here but the system can't really deal with that yet + // Move from qCDebug() to qCInfo() when we support controlling that + qCDebug(QLoggingCategory(self->category)).noquote() << strings.join(' '); + Py_RETURN_NONE; +} + +static PyObject * PyNymeaLoggingHandler_debug(PyNymeaLoggingHandler* self, PyObject* args) +{ + QStringList strings; + for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { + PyObject *obj = PyTuple_GET_ITEM(args, i); + PyObject* repr = PyObject_Repr(obj); + PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + const char *bytes = PyBytes_AS_STRING(str); + Py_XDECREF(repr); + Py_XDECREF(str); + strings.append(bytes); + } + qCDebug(QLoggingCategory(self->category)).noquote() << strings.join(' '); + Py_RETURN_NONE; +} + +static PyObject * PyNymeaLoggingHandler_warn(PyNymeaLoggingHandler* self, PyObject* args) +{ + QStringList strings; + for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { + PyObject *obj = PyTuple_GET_ITEM(args, i); + PyObject* repr = PyObject_Repr(obj); + PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + const char *bytes = PyBytes_AS_STRING(str); + Py_XDECREF(repr); + Py_XDECREF(str); + strings.append(bytes); + } + qCWarning(QLoggingCategory(self->category)).noquote() << strings.join(' '); + Py_RETURN_NONE; +} + + +static PyMethodDef PyNymeaLoggingHandler_methods[] = { + { "log", (PyCFunction)PyNymeaLoggingHandler_info, METH_VARARGS, "Log an info message to the nymea log. Same as info()." }, + { "info", (PyCFunction)PyNymeaLoggingHandler_info, METH_VARARGS, "Log an info message to the nymea log." }, + { "debug", (PyCFunction)PyNymeaLoggingHandler_debug, METH_VARARGS, "Log a debug message to the nymea log." }, + { "warn", (PyCFunction)PyNymeaLoggingHandler_warn, METH_VARARGS, "Log a warning message to the nymea log." }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyNymeaLoggingHandlerType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.NymeaLoggingHandler", /* tp_name */ + sizeof(PyNymeaLoggingHandler), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyNymeaLoggingHandler_dealloc,/* tp_dealloc */ +}; + + +static void registerNymeaLoggingHandler(PyObject *module) +{ + + PyNymeaLoggingHandlerType.tp_new = PyType_GenericNew; + PyNymeaLoggingHandlerType.tp_init = reinterpret_cast(PyNymeaLoggingHandler_init); + PyNymeaLoggingHandlerType.tp_flags = Py_TPFLAGS_DEFAULT; + PyNymeaLoggingHandlerType.tp_methods = PyNymeaLoggingHandler_methods; + PyNymeaLoggingHandlerType.tp_doc = "Logging handler for nymea."; + + if (PyType_Ready(&PyNymeaLoggingHandlerType) == 0) { + PyModule_AddObject(module, "NymeaLoggingHandler", (PyObject *)&PyNymeaLoggingHandlerType); + } +} + +#pragma GCC diagnostic pop + +#endif // PYNYMEALOGGINGHANDLER_H diff --git a/libnymea-core/integrations/python/pynymeamodule.h b/libnymea-core/integrations/python/pynymeamodule.h new file mode 100644 index 00000000..b9f01844 --- /dev/null +++ b/libnymea-core/integrations/python/pynymeamodule.h @@ -0,0 +1,58 @@ +#ifndef PYNYMEAMODULE_H +#define PYNYMEAMODULE_H + +#include + +#include "pystdouthandler.h" +#include "pynymealogginghandler.h" +#include "pything.h" +#include "pythingdiscoveryinfo.h" +#include "pythingsetupinfo.h" +#include "pyparam.h" +#include "pythingactioninfo.h" +#include "pythingpairinginfo.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" + +static int nymea_exec(PyObject *m) { + + registerStdOutHandler(m); + registerNymeaLoggingHandler(m); + registerParamType(m); + registerThingType(m); + registerThingDescriptorType(m); + registerThingDiscoveryInfoType(m); + registerThingPairingInfoType(m); + registerThingSetupInfoType(m); + registerThingActionInfoType(m); + + return 0; +} + +static struct PyModuleDef_Slot nymea_slots[] = { + {Py_mod_exec, (void*)nymea_exec}, + {0, NULL}, +}; + +static struct PyModuleDef nymea_module = { + PyModuleDef_HEAD_INIT, + "nymea", + "The nymea module. Provdes types used in the nymea plugin API.", + 0, + nullptr, // methods + nymea_slots, // slots + nullptr, + nullptr, + nullptr +}; + +PyMODINIT_FUNC PyInit_nymea(void) +{ + return PyModuleDef_Init(&nymea_module); +} + +#pragma GCC diagnostic pop + +#endif // PYNYMEAMODULE_H diff --git a/libnymea-core/integrations/python/pyparam.h b/libnymea-core/integrations/python/pyparam.h new file mode 100644 index 00000000..ac37583e --- /dev/null +++ b/libnymea-core/integrations/python/pyparam.h @@ -0,0 +1,148 @@ +#ifndef PYPARAM_H +#define PYPARAM_H + +#include +#include + +#include "pyutils.h" + +#include "types/param.h" +#include "types/paramtype.h" + +#include "loggingcategories.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + + +typedef struct _pyparam { + PyObject_HEAD + PyObject* pyParamTypeId = nullptr; + PyObject* pyValue = nullptr; +} PyParam; + +static PyMethodDef PyParam_methods[] = { + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyMemberDef PyParam_members[] = { + {"paramTypeId", T_OBJECT_EX, offsetof(PyParam, pyParamTypeId), 0, "Param type ID"}, + {"value", T_OBJECT_EX, offsetof(PyParam, pyValue), 0, "Param value"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + + +static int PyParam_init(PyParam *self, PyObject *args, PyObject *kwds) +{ + qCDebug(dcPythonIntegrations()) << "+++ PyParam"; + static char *kwlist[] = {"paramTypeId", "value", nullptr}; + PyObject *paramTypeId = nullptr, *value = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, ¶mTypeId, &value)) + return -1; + + if (paramTypeId) { + Py_INCREF(paramTypeId); + self->pyParamTypeId = paramTypeId; + } + if (value) { + Py_INCREF(value); + self->pyValue = value; + } + return 0; +} + +static void PyParam_dealloc(PyParam * self) { + qCDebug(dcPythonIntegrations()) << "--- PyParam"; + Py_XDECREF(self->pyParamTypeId); + Py_XDECREF(self->pyValue); + Py_TYPE(self)->tp_free(self); +} + +static PyTypeObject PyParamType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.Param", /* tp_name */ + sizeof(PyParam), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyParam_dealloc,/* tp_dealloc */ +}; + +static PyParam* PyParam_fromParam(const Param ¶m) +{ + PyObject *pyParamValue = QVariantToPyObject(param.value()); + PyObject *args = Py_BuildValue("(sO)", param.paramTypeId().toString().toUtf8().data(), pyParamValue); + + PyParam *pyParam = (PyParam*)PyObject_CallObject((PyObject*)&PyParamType, args); + + Py_DECREF(pyParamValue); + Py_DECREF(args); + + return pyParam; +} + +static Param PyParam_ToParam(PyParam *pyParam) +{ + ParamTypeId paramTypeId = ParamTypeId(PyUnicode_AsUTF8AndSize(pyParam->pyParamTypeId, nullptr)); + QVariant value = PyObjectToQVariant(pyParam->pyValue); + return Param(paramTypeId, value); +} + +static PyObject* PyParams_FromParamList(const ParamList ¶ms) +{ + PyObject* result = PyTuple_New(params.length()); + for (int i = 0; i < params.count(); i++) { + PyParam *pyParam = PyParam_fromParam(params.at(i)); + PyTuple_SetItem(result, i, (PyObject*)pyParam); + } + 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)); + Py_DECREF(next); + } + + Py_DECREF(iter); + return params; +} + +static void registerParamType(PyObject *module) +{ + PyParamType.tp_new = PyType_GenericNew; + PyParamType.tp_init = reinterpret_cast(PyParam_init); + PyParamType.tp_flags = Py_TPFLAGS_DEFAULT; + PyParamType.tp_methods = PyParam_methods; + PyParamType.tp_members = PyParam_members; + PyParamType.tp_doc = "Param class"; + + if (PyType_Ready(&PyParamType) < 0) { + return; + } + PyModule_AddObject(module, "Param", reinterpret_cast(&PyParamType)); +} + +#pragma GCC diagnostic pop + +#endif // PYPARAM_H diff --git a/libnymea-core/integrations/python/pystdouthandler.h b/libnymea-core/integrations/python/pystdouthandler.h new file mode 100644 index 00000000..24d3fdba --- /dev/null +++ b/libnymea-core/integrations/python/pystdouthandler.h @@ -0,0 +1,108 @@ +#ifndef PYSTDOUTHANDLER_H +#define PYSTDOUTHANDLER_H + +#include +#include "structmember.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(dcPythonIntegrations) + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +typedef struct { + PyObject_HEAD + char *category; + QtMsgType msgType; +} PyStdOutHandler; + +static int PyStdOutHandler_init(PyStdOutHandler *self, PyObject *args, PyObject */*kwds*/) +{ + qCDebug(dcPythonIntegrations()) << "+++ PyStdOutHandler"; + char *category = nullptr; + QtMsgType msgType; + if (!PyArg_ParseTuple(args, "si", &category, &msgType)) { + qCWarning(dcPythonIntegrations()) << "PyStdOutHandler: Error parsing parameters"; + return -1; + } + + self->category = (char*)malloc(qstrlen(category)); + self->msgType = msgType; + qstrcpy(self->category, category); + + return 0; +} + +static void PyStdOutHandler_dealloc(PyStdOutHandler * self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyStdOutHandler"; + free(self->category); + Py_TYPE(self)->tp_free(self); +} + +static PyObject* PyStdOutHandler_write(PyStdOutHandler* self, PyObject* args) +{ + const char *what; + if (!PyArg_ParseTuple(args, "s", &what)) + return nullptr; + if (!QByteArray(what).trimmed().isEmpty()) { + switch (self->msgType) { + case QtMsgType::QtInfoMsg: + qCInfo(QLoggingCategory(self->category)) << what; + break; + case QtMsgType::QtDebugMsg: + qCDebug(QLoggingCategory(self->category)) << what; + break; + case QtMsgType::QtWarningMsg: + qCWarning(QLoggingCategory(self->category)) << what; + break; + case QtMsgType::QtCriticalMsg: + qCCritical(QLoggingCategory(self->category)) << what; + break; + default: + qCDebug(QLoggingCategory(self->category)) << what; + break; + } + } + Py_RETURN_NONE; +} + +static PyObject* PyStdOutHandler_flush(PyObject* /*self*/, PyObject* /*args*/) +{ + // Not really needed... QDebug flushes already on its own + Py_RETURN_NONE; +} + +static PyMethodDef PyStdOutHandler_methods[] = { + { "write", (PyCFunction)PyStdOutHandler_write, METH_VARARGS, "Writes to stdout through qDebug()"}, + { "flush", (PyCFunction)PyStdOutHandler_flush, METH_VARARGS, "no-op"}, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyStdOutHandlerType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.StdOutHandler", /* tp_name */ + sizeof(PyStdOutHandler), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyStdOutHandler_dealloc,/* tp_dealloc */ +}; + +static void registerStdOutHandler(PyObject *module) +{ + + PyStdOutHandlerType.tp_new = PyType_GenericNew; + PyStdOutHandlerType.tp_init = reinterpret_cast(PyStdOutHandler_init); + PyStdOutHandlerType.tp_flags = Py_TPFLAGS_DEFAULT; + PyStdOutHandlerType.tp_methods = PyStdOutHandler_methods; + PyStdOutHandlerType.tp_doc = "Logging handler for nymea."; + + if (PyType_Ready(&PyStdOutHandlerType) == 0) { + PyModule_AddObject(module, "NymeaLoggingHandler", (PyObject *)&PyStdOutHandlerType); + } +} + +#endif // PYSTDOUTHANDLER_H diff --git a/libnymea-core/integrations/python/pything.h b/libnymea-core/integrations/python/pything.h new file mode 100644 index 00000000..1678e993 --- /dev/null +++ b/libnymea-core/integrations/python/pything.h @@ -0,0 +1,391 @@ +#ifndef PYTHING_H +#define PYTHING_H + +#include +#include "structmember.h" + +#include "pyparam.h" + +#include "integrations/thing.h" +#include "loggingcategories.h" + +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * When using this, make sure to call PyThing_setThing() while holding the GIL to initialize + * stuff after constructing it. + * + * The Thing class is not threadsafe and self->thing is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to hold the GIL whenver accessing the pointer value for invoking stuff. + * + * For reading access, we keep copies of the thing properties here and sync them + * over to the according py* members when they change. + * + */ + + +typedef struct _thing { + PyObject_HEAD + Thing *thing = nullptr; // the actual thing in nymea (not thread-safe!) + ThingClass *thingClass = nullptr; // A copy of the thing class. This is owned by the python thread + PyObject *pyId = nullptr; + PyObject *pyThingClassId = nullptr; + PyObject *pyName = nullptr; + PyObject *pyParams = nullptr; + PyObject *pySettings = nullptr; + PyObject *pyNameChangedHandler = nullptr; + PyObject *pySettingChangedHandler = nullptr; + PyObject *pyStates = nullptr; // A copy of the things states + PyThreadState *threadState = nullptr; // The python threadstate this thing belongs to +} PyThing; + + +static PyObject* PyThing_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) { + PyThing *self = (PyThing*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyThing" << self; + + return (PyObject*)self; +} + +static void PyThing_setThing(PyThing *self, Thing *thing, PyThreadState *threadState) +{ + self->thing = thing; + self->threadState = threadState; + + // Creating a copy because we cannot access the actual thing from the python thread + self->thingClass = new ThingClass(thing->thingClass()); + + self->pyId = PyUnicode_FromString(self->thing->id().toString().toUtf8().data()); + self->pyThingClassId = PyUnicode_FromString(self->thing->thingClassId().toString().toUtf8().data()); + self->pyName = PyUnicode_FromString(self->thing->name().toUtf8().data()); + self->pyParams = PyParams_FromParamList(self->thing->params()); + self->pySettings = PyParams_FromParamList(self->thing->settings()); + + self->pyStates = PyList_New(thing->states().count()); + for (int i = 0; i < thing->states().count(); i++) { + State state = thing->states().at(i); + PyObject *pyState = Py_BuildValue("{s:s, s:O}", + "stateTypeId", state.stateTypeId().toString().toUtf8().data(), + "value", QVariantToPyObject(state.value())); + PyList_SetItem(self->pyStates, i, pyState); + } + + + // Connects signal handlers from the Thing to sync stuff over to the pyThing in a + // thread-safe manner. + + // Those lambdas Will be executed in the main thread context. This means we + // can access self->thing, but need to hold the GIL for interacting with python + QObject::connect(thing, &Thing::nameChanged, [=](){ + PyEval_RestoreThread(self->threadState); + Py_XDECREF(self->pyName); + self->pyName = PyUnicode_FromString(self->thing->name().toUtf8().data()); + if (self->pyNameChangedHandler) { + PyObject *ret = PyObject_CallFunctionObjArgs(self->pyNameChangedHandler, self, nullptr); + if (PyErr_Occurred()) { + PyErr_Print(); + } + Py_XDECREF(ret); + } + PyEval_ReleaseThread(self->threadState); + }); + + + QObject::connect(thing, &Thing::settingChanged, [=](const ParamTypeId ¶mTypeId, const QVariant &value){ + PyEval_RestoreThread(self->threadState); + Py_XDECREF(self->pySettings); + self->pySettings = PyParams_FromParamList(self->thing->settings()); + if (self->pySettingChangedHandler) { + PyObject * ret = PyObject_CallFunctionObjArgs(self->pySettingChangedHandler, self, PyUnicode_FromString(paramTypeId.toString().toUtf8().data()), QVariantToPyObject(value), nullptr); + if (PyErr_Occurred()) { + PyErr_Print(); + } + Py_XDECREF(ret); + } + PyEval_ReleaseThread(self->threadState); + }); + + QObject::connect(thing, &Thing::stateValueChanged, [=](const StateTypeId &stateTypeId, const QVariant &value){ + PyEval_RestoreThread(self->threadState); + for (int i = 0; i < PyList_Size(self->pyStates); i++) { + PyObject *pyState = PyList_GetItem(self->pyStates, i); + PyObject *pyStateTypeId = PyDict_GetItemString(pyState, "stateTypeId"); + StateTypeId stid = StateTypeId(PyUnicode_AsUTF8AndSize(pyStateTypeId, nullptr)); + if (stid == stateTypeId) { + pyState = Py_BuildValue("{s:s, s:O}", + "stateTypeId", stateTypeId.toString().toUtf8().data(), + "value", QVariantToPyObject(value)); + PyList_SetItem(self->pyStates, i, pyState); + break; + } + } + PyEval_ReleaseThread(self->threadState); + }); +} + + +static void PyThing_dealloc(PyThing * self) { + qCDebug(dcPythonIntegrations()) << "--- PyThing" << self; + Py_XDECREF(self->pyId); + Py_XDECREF(self->pyThingClassId); + Py_XDECREF(self->pyName); + Py_XDECREF(self->pyParams); + Py_XDECREF(self->pySettings); + Py_XDECREF(self->pyStates); + Py_XDECREF(self->pyNameChangedHandler); + Py_XDECREF(self->pySettingChangedHandler); + delete self->thingClass; + Py_TYPE(self)->tp_free(self); +} + +static PyObject *PyThing_getName(PyThing *self, void */*closure*/) +{ + Py_INCREF(self->pyName); + return self->pyName; +} + +static PyObject *PyThing_getId(PyThing *self, void */*closure*/) +{ + Py_INCREF(self->pyId); + return self->pyId; +} + +static PyObject *PyThing_getThingClassId(PyThing *self, void */*closure*/) +{ + Py_INCREF(self->pyThingClassId); + return self->pyThingClassId; +} + +static int PyThing_setName(PyThing *self, PyObject *value, void */*closure*/){ + QString name = QString(PyUnicode_AsUTF8(value)); + if (!self->thing) { + return -1; + } + QMetaObject::invokeMethod(self->thing, "setName", Qt::QueuedConnection, Q_ARG(QString, name)); + return 0; +} + +static PyObject * PyThing_paramValue(PyThing* self, PyObject* args) +{ + char *paramTypeIdStr = nullptr; + + if (!PyArg_ParseTuple(args, "s", ¶mTypeIdStr)) { + qCWarning(dcThingManager) << "Error parsing parameters"; + return nullptr; + } + + ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr); + PyObject *iterator = PyObject_GetIter(self->pyParams); + while (iterator) { + PyObject *pyParam = PyIter_Next(iterator); + if (!pyParam) { + break; + } + + Param param = PyParam_ToParam((PyParam*)pyParam); + Py_DECREF(pyParam); + + if (param.paramTypeId() != paramTypeId) { + continue; + } + + Py_DECREF(iterator); + + return QVariantToPyObject(param.value()); + } + + Py_DECREF(iterator); + qCWarning(dcPythonIntegrations()) << "No param for paramTypeId:" << paramTypeId; + Py_RETURN_NONE; +} + +static PyObject * PyThing_setting(PyThing* self, PyObject* args) +{ + char *paramTypeIdStr = nullptr; + + if (!PyArg_ParseTuple(args, "s", ¶mTypeIdStr)) { + qCWarning(dcThingManager) << "Error parsing parameters"; + return nullptr; + } + + ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr); + PyObject *iterator = PyObject_GetIter(self->pySettings); + while (iterator) { + PyObject *pyParam = PyIter_Next(iterator); + if (!pyParam) { + break; + } + + Param param = PyParam_ToParam((PyParam*)pyParam); + Py_DECREF(pyParam); + + if (param.paramTypeId() != paramTypeId) { + continue; + } + + Py_DECREF(iterator); + + return QVariantToPyObject(param.value()); + } + + Py_DECREF(iterator); + qCWarning(dcPythonIntegrations()) << "No setting for paramTypeId:" << paramTypeId; + Py_RETURN_NONE; +} + +static PyObject *PyThing_getSettings(PyThing *self, void */*closure*/) +{ + Py_INCREF(self->pySettings); + return self->pySettings; +} + +static int PyThing_setSettings(PyThing */*self*/, PyObject */*value*/, void */*closure*/){ + // self->thing->setName(QString(PyUnicode_AsUTF8(value))); + return 0; +} + +static PyObject * PyThing_stateValue(PyThing* self, PyObject* args) +{ + char *stateTypeIdStr = nullptr; + + if (!PyArg_ParseTuple(args, "s", &stateTypeIdStr)) { + PyErr_SetString(PyExc_ValueError, "Error parsing arguments. Signature is 's'"); + return nullptr; + } + + StateTypeId stateTypeId = StateTypeId(stateTypeIdStr); + + for (int i = 0; i < PyList_Size(self->pyStates); i++) { + PyObject *pyState = PyList_GetItem(self->pyStates, i); + PyObject *pyStateTypeId = PyDict_GetItemString(pyState, "stateTypeId"); + StateTypeId stid = StateTypeId(PyUnicode_AsUTF8AndSize(pyStateTypeId, nullptr)); + if (stid == stateTypeId) { + PyObject *value = PyDict_GetItemString(pyState, "value"); + Py_INCREF(value); + return value; + } + } + + PyErr_SetString(PyExc_ValueError, QString("No state type %1 in thing class %2").arg(stateTypeId.toString()).arg(self->thingClass->name()).toUtf8()); + return nullptr; +} + +static PyObject * PyThing_setStateValue(PyThing* self, PyObject* args) +{ + char *stateTypeIdStr = nullptr; + PyObject *valueObj = nullptr; + + if (!PyArg_ParseTuple(args, "sO", &stateTypeIdStr, &valueObj)) { + PyErr_SetString(PyExc_ValueError, "Error parsing arguments. Signature is 'sO'"); + return nullptr; + } + + StateTypeId stateTypeId = StateTypeId(stateTypeIdStr); + QVariant value = PyObjectToQVariant(valueObj); + + if (self->thing != nullptr) { + QMetaObject::invokeMethod(self->thing, "setStateValue", Qt::QueuedConnection, Q_ARG(StateTypeId, stateTypeId), Q_ARG(QVariant, value)); + } + + Py_RETURN_NONE; +} + +static PyObject * PyThing_emitEvent(PyThing* self, PyObject* args) +{ + char *eventTypeIdStr = nullptr; + PyObject *valueObj = nullptr; + + if (!PyArg_ParseTuple(args, "s|O", &eventTypeIdStr, &valueObj)) { + PyErr_SetString(PyExc_TypeError, "Supplied arguments for emitEvent must be a ParamList"); + return nullptr; + } + if (qstrcmp(valueObj->ob_type->tp_name, "list") != 0) { + PyErr_SetString(PyExc_TypeError, "Supplied arguments for emitEvent must be a ParamList"); + return nullptr; + } + + EventTypeId eventTypeId = EventTypeId(eventTypeIdStr); + EventType eventType = self->thingClass->eventTypes().findById(eventTypeId); + if (!eventType.isValid()) { + PyErr_SetString(PyExc_ValueError, QString("No event type %1 in thing class %2").arg(eventTypeId.toString()).arg(self->thingClass->name()).toUtf8()); + return nullptr; + } + + ParamList params = PyParams_ToParamList(valueObj); + + if (self->thing != nullptr) { + QMetaObject::invokeMethod(self->thing, "emitEvent", Qt::QueuedConnection, Q_ARG(EventTypeId, eventTypeId), Q_ARG(ParamList, params)); + } + + Py_RETURN_NONE; +} + +static PyGetSetDef PyThing_getset[] = { + {"name", (getter)PyThing_getName, (setter)PyThing_setName, "Thing name", nullptr}, + {"id", (getter)PyThing_getId, 0, "ThingId", nullptr}, + {"thingClassId", (getter)PyThing_getThingClassId, 0, "ThingClassId", nullptr}, + {"settings", (getter)PyThing_getSettings, (setter)PyThing_setSettings, "Thing settings", nullptr}, + {nullptr , nullptr, nullptr, nullptr, nullptr} /* Sentinel */ +}; + +static PyMethodDef PyThing_methods[] = { + { "paramValue", (PyCFunction)PyThing_paramValue, METH_VARARGS, "Get a things param value by paramTypeId" }, + { "setting", (PyCFunction)PyThing_setting, METH_VARARGS, "Get a things setting value by paramTypeId" }, + { "stateValue", (PyCFunction)PyThing_stateValue, METH_VARARGS, "Get a things state value by stateTypeId" }, + { "setStateValue", (PyCFunction)PyThing_setStateValue, METH_VARARGS, "Set a certain things state value by stateTypeIp" }, + { "emitEvent", (PyCFunction)PyThing_emitEvent, METH_VARARGS, "Emits an event" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyMemberDef PyThing_members[] = { + {"params", T_OBJECT_EX, offsetof(PyThing, pyParams), READONLY, "Thing params"}, + {"nameChangedHandler", T_OBJECT_EX, offsetof(PyThing, pyNameChangedHandler), 0, "Set a callback for when the thing name changes"}, + {"settingChangedHandler", T_OBJECT_EX, offsetof(PyThing, pySettingChangedHandler), 0, "Set a callback for when a thing setting changes"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static PyTypeObject PyThingType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.Thing", /* tp_name */ + sizeof(PyThing), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyThing_dealloc, /* tp_dealloc */ +}; + +static void registerThingType(PyObject *module) +{ + PyThingType.tp_new = (newfunc)PyThing_new; + PyThingType.tp_flags = Py_TPFLAGS_DEFAULT; + PyThingType.tp_methods = PyThing_methods; + PyThingType.tp_members = PyThing_members; + PyThingType.tp_getset = PyThing_getset; + PyThingType.tp_doc = "The Thing class represents a thing in nymea."; + + if (PyType_Ready(&PyThingType) < 0) { + return; + } + PyModule_AddObject(module, "Thing", reinterpret_cast(&PyThingType)); + + QMetaEnum thingErrorEnum = QMetaEnum::fromType(); + for (int i = 0; i < thingErrorEnum.keyCount(); i++) { + PyModule_AddObject(module, thingErrorEnum.key(i), PyLong_FromLong(thingErrorEnum.value(i))); + } +} + + +#pragma GCC diagnostic pop + +#endif // PYTHING_H diff --git a/libnymea-core/integrations/python/pythingactioninfo.h b/libnymea-core/integrations/python/pythingactioninfo.h new file mode 100644 index 00000000..6dd90aa8 --- /dev/null +++ b/libnymea-core/integrations/python/pythingactioninfo.h @@ -0,0 +1,148 @@ +#ifndef PYTHINGACTIONINFO_H +#define PYTHINGACTIONINFO_H + +#include +#include "structmember.h" + +#include "pything.h" + +#include "integrations/thingactioninfo.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * + * When using this, make sure to call PyThingActionInfo_setInfo() while holding the GIL to initialize + * stuff after constructing it. Also set info to nullptr while holding the GIL when the info object vanishes. + * + * The ThingActionInfo class is not threadsafe and self->info is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the setup and destroyed it but the PyThingSetupInfo is not garbage collected yet. + * + * For reading access, we keep copies of the thing properties here and sync them + * over to the according py* members when they change. + * + */ + +typedef struct { + PyObject_HEAD + ThingActionInfo* info; + PyThing *pyThing; + PyObject *pyActionTypeId; + PyObject *pyParams; +} PyThingActionInfo; + + +static PyObject* PyThingActionInfo_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) { + PyThingActionInfo *self = (PyThingActionInfo*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyThingActionInfo"; + return (PyObject*)self; +} + +void PyThingActionInfo_setInfo(PyThingActionInfo *self, ThingActionInfo *info, PyThing *pyThing) +{ + self->info = info; + self->pyThing = pyThing; + Py_INCREF(pyThing); + self->pyActionTypeId = PyUnicode_FromString(info->action().actionTypeId().toString().toUtf8()); + self->pyParams = PyParams_FromParamList(info->action().params()); +} + + +static void PyThingActionInfo_dealloc(PyThingActionInfo * self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyThingActionInfo"; + Py_DECREF(self->pyThing); + Py_DECREF(self->pyActionTypeId); + Py_DECREF(self->pyParams); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * PyThingActionInfo_finish(PyThingActionInfo* self, PyObject* args) { + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "i|s", &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(ThingError, message = \"\")"); + return nullptr; + } + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->info) { + QMetaObject::invokeMethod(self->info, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + + Py_RETURN_NONE; +} + +static PyObject * PyThingActionInfo_paramValue(PyThingActionInfo* self, PyObject* args) { + char *paramTypeIdStr = nullptr; + if (!PyArg_ParseTuple(args, "s", ¶mTypeIdStr)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in paramValue call. Expected: paramValue(paramTypeId)"); + return nullptr; + } + + ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr); + for (int i = 0; i < PyTuple_Size(self->pyParams); i++) { + PyParam *pyParam = reinterpret_cast(PyTuple_GetItem(self->pyParams, i)); + // We're intentionally converting both ids to QUuid here in order to be more flexible with different UUID notations + ParamTypeId ptid = StateTypeId(PyUnicode_AsUTF8AndSize(pyParam->pyParamTypeId, nullptr)); + if (ptid == paramTypeId) { + Py_INCREF(pyParam->pyValue); + return pyParam->pyValue; + } + } + qCWarning(dcPythonIntegrations()) << "No such ParamTypeId in action params" << paramTypeId; + Py_RETURN_NONE; +}; + +static PyMemberDef PyThingActionInfo_members[] = { + {"thing", T_OBJECT_EX, offsetof(PyThingActionInfo, pyThing), 0, "Thing this action is for"}, + {"actionTypeId", T_OBJECT_EX, offsetof(PyThingActionInfo, pyActionTypeId), 0, "The action type id for this action"}, + {"params", T_OBJECT_EX, offsetof(PyThingActionInfo, pyParams), 0, "The params for this action"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static PyMethodDef PyThingActionInfo_methods[] = { + { "finish", (PyCFunction)PyThingActionInfo_finish, METH_VARARGS, "finish an action" }, + { "paramValue", (PyCFunction)PyThingActionInfo_paramValue, METH_VARARGS, "Get an actions param value"}, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyThingActionInfoType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.ThingActionInfo", /* tp_name */ + sizeof(PyThingActionInfo), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyThingActionInfo_dealloc, /* tp_dealloc */ +}; + +static void registerThingActionInfoType(PyObject *module) +{ + PyThingActionInfoType.tp_new = (newfunc)PyThingActionInfo_new; + PyThingActionInfoType.tp_flags = Py_TPFLAGS_DEFAULT; + PyThingActionInfoType.tp_methods = PyThingActionInfo_methods; + PyThingActionInfoType.tp_members = PyThingActionInfo_members; + PyThingActionInfoType.tp_doc = "The ThingActionInfo is used to dispatch actions to things"; + + if (PyType_Ready(&PyThingActionInfoType) < 0) { + return; + } + PyModule_AddObject(module, "ThingActionInfo", (PyObject *)&PyThingActionInfoType); +} + + +#pragma GCC diagnostic pop + +#endif // PYTHINGACTIONINFO_H diff --git a/libnymea-core/integrations/python/pythingdescriptor.h b/libnymea-core/integrations/python/pythingdescriptor.h new file mode 100644 index 00000000..e27d0440 --- /dev/null +++ b/libnymea-core/integrations/python/pythingdescriptor.h @@ -0,0 +1,104 @@ +#ifndef PYTHINGDESCRIPTOR_H +#define PYTHINGDESCRIPTOR_H + +#include +#include "structmember.h" + +#include "integrations/thingdescriptor.h" +#include "loggingcategories.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + + +typedef struct { + PyObject_HEAD + PyObject* pyThingClassId; + PyObject* pyName; + PyObject* pyDescription; + PyObject* pyThingId; + PyObject* pyParams; +} PyThingDescriptor; + + +static PyMemberDef PyThingDescriptor_members[] = { + {"thingClassId", T_OBJECT_EX, offsetof(PyThingDescriptor, pyThingClassId), 0, "Descriptor thingClassId"}, + {"name", T_OBJECT_EX, offsetof(PyThingDescriptor, pyName), 0, "Descriptor name"}, + {"description", T_OBJECT_EX, offsetof(PyThingDescriptor, pyDescription), 0, "Descriptor description"}, + {"thingId", T_OBJECT_EX, offsetof(PyThingDescriptor, pyDescription), 0, "The thingId, if there exists a thing for this descriptor already."}, + {"params", T_OBJECT_EX, offsetof(PyThingDescriptor, pyParams), 0, "Params for the thing described by this descriptor."}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static int PyThingDescriptor_init(PyThingDescriptor *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"thingClassId", "name", "description", "thingId", "params", nullptr}; + PyObject *thingClassId = nullptr, *name = nullptr, *description = nullptr, *thingId = nullptr, *params = nullptr; + + qCDebug(dcPythonIntegrations()) << "+++ PyThingDescriptor"; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, &thingClassId, &name, &description, &thingId, ¶ms)) + return -1; + + if (thingClassId) { + Py_INCREF(thingClassId); + self->pyThingClassId = thingClassId; + } + if (name) { + Py_INCREF(name); + self->pyName = name; + } + if (description) { + Py_INCREF(description); + self->pyDescription = description; + } + if (thingId) { + Py_INCREF(thingId); + self->pyThingId = thingId; + } + if (params) { + Py_INCREF(params); + self->pyParams = params; + } + return 0; +} + +static void PyThingDescriptor_dealloc(PyThingDescriptor * self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyThingDescriptor"; + Py_XDECREF(self->pyThingClassId); + Py_XDECREF(self->pyName); + Py_XDECREF(self->pyDescription); + Py_XDECREF(self->pyThingId); + Py_XDECREF(self->pyParams); + Py_TYPE(self)->tp_free(self); +} + +static PyTypeObject PyThingDescriptorType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.ThingDescriptor", /* tp_name */ + sizeof(PyThingDescriptor), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyThingDescriptor_dealloc, /* tp_dealloc */ +}; + + + +static void registerThingDescriptorType(PyObject *module) +{ + PyThingDescriptorType.tp_new = PyType_GenericNew; + PyThingDescriptorType.tp_members = PyThingDescriptor_members; + PyThingDescriptorType.tp_init = reinterpret_cast(PyThingDescriptor_init); + PyThingDescriptorType.tp_doc = "ThingDescriptors are used to inform the system about things that may be added."; + PyThingDescriptorType.tp_flags = Py_TPFLAGS_DEFAULT; + + if (PyType_Ready(&PyThingDescriptorType) < 0) { + return; + } + PyModule_AddObject(module, "ThingDescriptor", reinterpret_cast(&PyThingDescriptorType)); +} + +#pragma GCC diagnostic pop + +#endif // PYTHINGDESCRIPTOR_H diff --git a/libnymea-core/integrations/python/pythingdiscoveryinfo.h b/libnymea-core/integrations/python/pythingdiscoveryinfo.h new file mode 100644 index 00000000..2b6a4e56 --- /dev/null +++ b/libnymea-core/integrations/python/pythingdiscoveryinfo.h @@ -0,0 +1,175 @@ +#ifndef PYTHINGDISCOVERYINFO_H +#define PYTHINGDISCOVERYINFO_H + + +#include +#include "structmember.h" + +#include "pythingdescriptor.h" +#include "pyparam.h" + +#include "integrations/thingdiscoveryinfo.h" + +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + + +/* Note: + * When using this, make sure to call PyThingDiscoveryInfo_setInfo() while holding the GIL to initialize + * stuff after constructing it. Also set info to nullptr while holding the GIL when the info object vanishes. + * + * The ThingDiscoveryInfo class is not threadsafe and self->info is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the discovery and destroyed it but the PyThingDiscoveryInfo is not garbage collected yet. + * + * For reading access, we keep copies of the thing properties here and sync them + * over to the according py* members when they change. + * + */ + + +typedef struct { + PyObject_HEAD + ThingDiscoveryInfo* info; + PyObject* pyThingClassId = nullptr; + PyObject *pyParams = nullptr; +} PyThingDiscoveryInfo; + +static PyObject* PyThingDiscoveryInfo_new(PyTypeObject *type, PyObject */*args*/, PyObject */*kwds*/) +{ + PyThingDiscoveryInfo *self = (PyThingDiscoveryInfo*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyThingDiscoveryInfo"; + return (PyObject*)self; +} + +void PyThingDiscoveryInfo_setInfo(PyThingDiscoveryInfo *self, ThingDiscoveryInfo *info) +{ + self->info = info; + self->pyThingClassId = PyUnicode_FromString(info->thingClassId().toString().toUtf8().data()); + self->pyParams = PyParams_FromParamList(info->params()); +} + +static void PyThingDiscoveryInfo_dealloc(PyThingDiscoveryInfo * self) +{ + qCDebug(dcPythonIntegrations()) << "--- PyThingDiscoveryInfo"; + Py_DECREF(self->pyThingClassId); + Py_DECREF(self->pyParams); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * PyThingDiscoveryInfo_finish(PyThingDiscoveryInfo* self, PyObject* args) +{ + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "i|s", &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(ThingError, message = \"\""); + return nullptr; + } + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->info) { + QMetaObject::invokeMethod(self->info, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + Py_RETURN_NONE; +} + +static PyObject * PyThingDiscoveryInfo_addDescriptor(PyThingDiscoveryInfo* self, PyObject* args) +{ + PyObject *pyObj = nullptr; + + if (!PyArg_ParseTuple(args, "O", &pyObj)) { + PyErr_SetString(PyExc_ValueError, "Invalid argument to ThingDiscoveryInfo.addDescriptor(). Not a ThingDescriptor."); + return nullptr; + } + if (pyObj->ob_type != &PyThingDescriptorType) { + PyErr_SetString(PyExc_ValueError, "Invalid argument to ThingDiscoveryInfo.addDescriptor(). Not a ThingDescriptor."); + return nullptr; + } + PyThingDescriptor *pyDescriptor = (PyThingDescriptor*)pyObj; + + 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); + if (pyDescriptor->pyThingId) { + descriptor.setThingId(ThingId(QString::fromUtf8(PyUnicode_AsUTF8(pyDescriptor->pyThingId)))); + } + + if (pyDescriptor->pyParams) { + descriptor.setParams(PyParams_ToParamList(pyDescriptor->pyParams)); + } + + if (self->info) { + QMetaObject::invokeMethod(self->info, "addThingDescriptor", Qt::QueuedConnection, Q_ARG(ThingDescriptor, descriptor)); + } + + Py_RETURN_NONE; +} + +static PyMemberDef PyThingDiscoveryInfo_members[] = { + {"thingClassId", T_OBJECT_EX, offsetof(PyThingDiscoveryInfo, pyThingClassId), READONLY, "The ThingClassId this discovery is for."}, + {"params", T_OBJECT_EX, offsetof(PyThingDiscoveryInfo, pyParams), READONLY, "The params for this discovery"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +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" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyThingDiscoveryInfoType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.ThingDiscoveryInfo", /* tp_name */ + sizeof(PyThingDiscoveryInfo), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyThingDiscoveryInfo_dealloc, /* tp_dealloc */ +}; + + + +static void registerThingDiscoveryInfoType(PyObject *module) +{ + PyThingDiscoveryInfoType.tp_new = (newfunc)PyThingDiscoveryInfo_new; + PyThingDiscoveryInfoType.tp_flags = Py_TPFLAGS_DEFAULT; + PyThingDiscoveryInfoType.tp_methods = PyThingDiscoveryInfo_methods; + PyThingDiscoveryInfoType.tp_members = PyThingDiscoveryInfo_members; + PyThingDiscoveryInfoType.tp_doc = "The ThingDiscoveryInfo is used to perform discoveries of things."; + + if (PyType_Ready(&PyThingDiscoveryInfoType) < 0) { + return; + } + PyModule_AddObject(module, "ThingDiscoveryInfo", (PyObject *)&PyThingDiscoveryInfoType); +} + + + + +#pragma GCC diagnostic pop + +#endif // PYTHINGDISCOVERYINFO_H diff --git a/libnymea-core/integrations/python/pythingpairinginfo.h b/libnymea-core/integrations/python/pythingpairinginfo.h new file mode 100644 index 00000000..7a9a8071 --- /dev/null +++ b/libnymea-core/integrations/python/pythingpairinginfo.h @@ -0,0 +1,148 @@ +#ifndef PYTHINGPAIRINGINFO_H +#define PYTHINGPAIRINGINFO_H + + +#include +#include "structmember.h" + +#include "pyparam.h" + +#include "integrations/thingpairinginfo.h" + +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * When using this, make sure to call PyThingPairingInfo_setInfo() while holding the GIL to initialize + * stuff after constructing it. Also set info to nullptr while holding the GIL when the info object vanishes. + * + * The ThingPairingInfo class is not threadsafe and self->info is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the pairing step and destroyed it but the PyThingPairingInfo is not garbage collected yet. + * + * For reading access, we keep copies of the thing properties here and sync them + * over to the according py* members when they change. + * + */ + + + +typedef struct { + PyObject_HEAD + ThingPairingInfo* info; + PyObject *pyTransactionId = nullptr; + PyObject *pyThingClassId = nullptr; + PyObject *pyThingId = nullptr; + PyObject *pyThingName = nullptr; + PyObject *pyParentId = nullptr; + PyObject *pyParams = nullptr; + PyObject *pyOAuthUrl = nullptr; +} PyThingPairingInfo; + +static PyMemberDef PyThingPairingInfo_members[] = { + {"transactionId", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyTransactionId), READONLY, "The transaction id for this pairing procedure."}, + {"thingClassId", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyThingClassId), READONLY, "The ThingClassId for the thing to be set up."}, + {"thingId", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyThingId), READONLY, "The ThingId for the thing to be set up."}, + {"thingName", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyThingName), READONLY, "The ThingId for the thing to be set up."}, + {"parentId", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyParentId), READONLY, "The ThingId for the parent of the thing to be set up."}, + {"params", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyParams), READONLY, "The params for the thing to be set up."}, + {"oAuthUrl", T_OBJECT_EX, offsetof(PyThingPairingInfo, pyOAuthUrl), 0, "An OAuth url if required for the pairing."}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static int PyThingPairingInfo_init(PyThingPairingInfo */*self*/, PyObject */*args*/, PyObject */*kwds*/) +{ + qCDebug(dcPythonIntegrations()) << "+++ ThingPairingInfo"; + return 0; +} + +void PyThingPairingInfo_setInfo(PyThingPairingInfo *self, ThingPairingInfo *info) +{ + self->info = info; + self->pyTransactionId = PyUnicode_FromString(info->transactionId().toString().toUtf8()); + self->pyThingClassId = PyUnicode_FromString(info->thingClassId().toString().toUtf8()); + self->pyThingId = PyUnicode_FromString(info->thingId().toString().toUtf8()); + self->pyThingName = PyUnicode_FromString(info->thingName().toUtf8()); + self->pyParentId = PyUnicode_FromString(info->parentId().toString().toUtf8()); + self->pyParams = PyParams_FromParamList(info->params()); +} + +static void PyThingPairingInfo_dealloc(PyThingPairingInfo * self) +{ + qCDebug(dcPythonIntegrations()) << "--- ThingPairingInfo"; + Py_XDECREF(self->pyTransactionId); + Py_XDECREF(self->pyThingClassId); + Py_XDECREF(self->pyThingId); + Py_XDECREF(self->pyThingName); + Py_XDECREF(self->pyParentId); + Py_XDECREF(self->pyParams); + Py_XDECREF(self->pyOAuthUrl); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * PyThingPairingInfo_finish(PyThingPairingInfo* self, PyObject* args) +{ + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "i|s", &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(ThingError, message = \"\""); + return nullptr; + } + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->info) { + if (self->pyOAuthUrl) { + QString oAuthUrl = QString::fromUtf8(PyUnicode_AsUTF8AndSize(self->pyOAuthUrl, nullptr)); + QMetaObject::invokeMethod(self->info, "setOAuthUrl", Qt::QueuedConnection, Q_ARG(QString, oAuthUrl)); + } + QMetaObject::invokeMethod(self->info, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + Py_RETURN_NONE; +} + +static PyMethodDef PyThingPairingInfo_methods[] = { + { "finish", (PyCFunction)PyThingPairingInfo_finish, METH_VARARGS, "Finish a discovery" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyThingPairingInfoType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.ThingPairingInfo", /* tp_name */ + sizeof(PyThingPairingInfo), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyThingPairingInfo_dealloc, /* tp_dealloc */ +}; + +static void registerThingPairingInfoType(PyObject *module) +{ + PyThingPairingInfoType.tp_new = PyType_GenericNew; + PyThingPairingInfoType.tp_init = (initproc)PyThingPairingInfo_init; + PyThingPairingInfoType.tp_flags = Py_TPFLAGS_DEFAULT; + PyThingPairingInfoType.tp_methods = PyThingPairingInfo_methods; + PyThingPairingInfoType.tp_members = PyThingPairingInfo_members; + PyThingPairingInfoType.tp_doc = "The ThingPairingInfo is used to aithenticate with a thing."; + + if (PyType_Ready(&PyThingPairingInfoType) < 0) { + return; + } + PyModule_AddObject(module, "ThingPairingInfo", (PyObject *)&PyThingPairingInfoType); +} + + + + +#pragma GCC diagnostic pop + +#endif // PYTHINGPAIRINGINFO_H diff --git a/libnymea-core/integrations/python/pythingsetupinfo.h b/libnymea-core/integrations/python/pythingsetupinfo.h new file mode 100644 index 00000000..bad30f43 --- /dev/null +++ b/libnymea-core/integrations/python/pythingsetupinfo.h @@ -0,0 +1,125 @@ +#ifndef PYTHINGSETUPINFO_H +#define PYTHINGSETUPINFO_H + +#include +#include "structmember.h" + +#include "pything.h" + +#include "integrations/thingsetupinfo.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" +#pragma GCC diagnostic ignored "-Wwrite-strings" +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +/* Note: + * + * When using this, make sure to call PyThingSetupInfo_setInfo() while holding the GIL to initialize + * stuff after constructing it. Also set info to nullptr while holding the GIL when the info object vanishes. + * + * The ThingSetupInfo class is not threadsafe and self->info is owned by nymeas main thread. + * So we must never directly access anything of it in here. + * + * For writing to it, invoking methods with QueuedConnections will thread-decouple stuff. + * Make sure to check if the info object is still valid (it might not be if nymea finished + * the setup and destroyed it but the PyThingSetupInfo is not garbage collected yet. + * + * For reading access, we keep copies of the thing properties here and sync them + * over to the according py* members when they change. + * + */ + +typedef struct { + PyObject_HEAD + ThingSetupInfo* info; + PyThing *pyThing; +} PyThingSetupInfo; + + +static PyObject* PyThingSetupInfo_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + PyThingSetupInfo *self = (PyThingSetupInfo*)type->tp_alloc(type, 0); + if (self == NULL) { + return nullptr; + } + qCDebug(dcPythonIntegrations()) << "+++ PyThingSetupInfo"; + + + static char *kwlist[] = {"thing", nullptr}; + PyObject *pyThing = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &pyThing)) { + PyErr_Print(); + PyErr_SetString(PyExc_ValueError, "Invalid arguments."); + return nullptr; + } + + self->pyThing = (PyThing*)pyThing; + Py_INCREF(self->pyThing); + + return (PyObject*)self; +} + +static void PyThingSetupInfo_dealloc(PyThingSetupInfo * self) { + qCDebug(dcPythonIntegrations()) << "--- PyThingSetupInfo"; + Py_DECREF(self->pyThing); + Py_TYPE(self)->tp_free(self); +} + +static PyObject * PyThingSetupInfo_finish(PyThingSetupInfo* self, PyObject* args) { + int status; + char *message = nullptr; + + if (!PyArg_ParseTuple(args, "i|s", &status, &message)) { + PyErr_SetString(PyExc_TypeError, "Invalid arguments in finish call. Expected: finish(ThingError, message = \"\""); + return nullptr; + } + + Thing::ThingError thingError = static_cast(status); + QString displayMessage = message != nullptr ? QString(message) : QString(); + + if (self->info) { + QMetaObject::invokeMethod(self->info, "finish", Qt::QueuedConnection, Q_ARG(Thing::ThingError, thingError), Q_ARG(QString, displayMessage)); + } + + Py_RETURN_NONE; +} + +static PyMemberDef PyThingSetupInfo_members[] = { + {"thing", T_OBJECT_EX, offsetof(PyThingSetupInfo, pyThing), 0, "Thing being setup in this setup transaction"}, + {nullptr, 0, 0, 0, nullptr} /* Sentinel */ +}; + +static PyMethodDef PyThingSetupInfo_methods[] = { + { "finish", (PyCFunction)PyThingSetupInfo_finish, METH_VARARGS, "finish a setup" }, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +static PyTypeObject PyThingSetupInfoType = { + PyVarObject_HEAD_INIT(NULL, 0) + "nymea.ThingSetupInfo", /* tp_name */ + sizeof(PyThingSetupInfo), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)PyThingSetupInfo_dealloc, /* tp_dealloc */ +}; + +static void registerThingSetupInfoType(PyObject *module) +{ + PyThingSetupInfoType.tp_new = (newfunc)PyThingSetupInfo_new; + PyThingSetupInfoType.tp_flags = Py_TPFLAGS_DEFAULT; + PyThingSetupInfoType.tp_methods = PyThingSetupInfo_methods; + PyThingSetupInfoType.tp_members = PyThingSetupInfo_members; + PyThingSetupInfoType.tp_doc = "The ThingSetupInfo is used to set up a thing."; + + if (PyType_Ready(&PyThingSetupInfoType) < 0) { + return; + } + PyModule_AddObject(module, "ThingSetupInfo", (PyObject *)&PyThingSetupInfoType); +} + + + + +#pragma GCC diagnostic pop + +#endif // PYTHINGSETUPINFO_H diff --git a/libnymea-core/integrations/python/pyutils.h b/libnymea-core/integrations/python/pyutils.h new file mode 100644 index 00000000..996183c7 --- /dev/null +++ b/libnymea-core/integrations/python/pyutils.h @@ -0,0 +1,75 @@ +#ifndef PYUTILS_H +#define PYUTILS_H + +#include + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(dcPythonIntegrations) + +/* Returns a new reference to PyObject*. */ +PyObject *QVariantToPyObject(const QVariant &value) +{ + PyObject *pyValue = nullptr; + + switch (value.type()) { + case QVariant::Bool: + pyValue = PyBool_FromLong(value.toBool()); + break; + case QVariant::Int: + case QVariant::UInt: + case QVariant::LongLong: + case QVariant::ULongLong: + pyValue = PyLong_FromLongLong(value.toLongLong()); + 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(dcPythonIntegrations()) << "Unhandled data type in conversion from Param to PyParam!"; + pyValue = Py_None; + Py_INCREF(pyValue); + break; + } + + return pyValue; +} + +QVariant PyObjectToQVariant(PyObject *pyObject) +{ + if (qstrcmp(pyObject->ob_type->tp_name, "int") == 0) { + return QVariant(PyLong_AsLongLong(pyObject)); + } + + if (qstrcmp(pyObject->ob_type->tp_name, "str") == 0) { + return QVariant(PyUnicode_AsUTF8AndSize(pyObject, nullptr)); + } + + if (qstrcmp(pyObject->ob_type->tp_name, "double") == 0) { + return QVariant(PyFloat_AsDouble(pyObject)); + } + + if (qstrcmp(pyObject->ob_type->tp_name, "float") == 0) { + return QVariant(PyFloat_AsDouble(pyObject)); + } + + if (qstrcmp(pyObject->ob_type->tp_name, "bool") == 0) { + return QVariant(PyObject_IsTrue(pyObject)); + } + + Q_ASSERT_X(false, "pyutils.h", QString("Unhandled data type in converting PyObject to QVariant: %1").arg(pyObject->ob_type->tp_name).toUtf8()); + qCWarning(dcPythonIntegrations()) << QString("Unhandled data type in converting PyObject to QVariant: %1").arg(pyObject->ob_type->tp_name).toUtf8(); + return QVariant(); +} + + +#endif // PYUTILS_H diff --git a/libnymea-core/integrations/pythonintegrationplugin.cpp b/libnymea-core/integrations/pythonintegrationplugin.cpp new file mode 100644 index 00000000..4bcec22a --- /dev/null +++ b/libnymea-core/integrations/pythonintegrationplugin.cpp @@ -0,0 +1,705 @@ +#include + +#include "pythonintegrationplugin.h" +#include "python/pynymeamodule.h" +#include "python/pystdouthandler.h" + +#include "loggingcategories.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +NYMEA_LOGGING_CATEGORY(dcPythonIntegrations, "PythonIntegrations") + +PyThreadState* PythonIntegrationPlugin::s_mainThreadState = nullptr; +QHash PythonIntegrationPlugin::s_plugins; + +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); + PyThing *pyThing = plugin->m_things.value(thing); + Py_INCREF(pyThing); + PyTuple_SET_ITEM(result, i, (PyObject*)pyThing); + } + plugin->m_mutex.unlock(); + return result; +} + +PyObject *PythonIntegrationPlugin::pyAutoThingsAppeared(PyObject *self, PyObject *args) +{ + PyObject *pyDescriptors; + + if (!PyArg_ParseTuple(args, "O", &pyDescriptors)) { + qCWarning(dcThingManager()) << "Error parsing args. Not a param list"; + return nullptr; + } + + PyObject *iter = PyObject_GetIter(pyDescriptors); + 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."); + Py_DECREF(next); + continue; + } + 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); + + if (pyDescriptor->pyParams) { + descriptor.setParams(PyParams_ToParamList(pyDescriptor->pyParams)); + } + + descriptors.append(descriptor); + Py_DECREF(next); + } + + PythonIntegrationPlugin *plugin = s_plugins.key(self); + QMetaObject::invokeMethod(plugin, "autoThingsAppeared", Qt::QueuedConnection, Q_ARG(ThingDescriptors, descriptors)); + + Py_DECREF(iter); + Py_RETURN_NONE; +} + +PyObject *PythonIntegrationPlugin::pyAutoThingDisappeared(PyObject *self, PyObject *args) +{ + char *thingIdStr = nullptr; + + if (!PyArg_ParseTuple(args, "s", &thingIdStr)) { + qCWarning(dcThingManager) << "Error parsing parameters"; + return nullptr; + } + ThingId thingId(thingIdStr); + PythonIntegrationPlugin *plugin = s_plugins.key(self); + QMetaObject::invokeMethod(plugin, "autoThingDisappeared", Qt::QueuedConnection, Q_ARG(ThingId, thingId)); + + 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."}, + {"autoThingDisappeared", PythonIntegrationPlugin::pyAutoThingDisappeared, METH_VARARGS, "Inform the system about auto setup things having disappeared."}, + {nullptr, nullptr, 0, nullptr} // sentinel +}; + +PythonIntegrationPlugin::PythonIntegrationPlugin(QObject *parent) : IntegrationPlugin(parent) +{ + +} + +PythonIntegrationPlugin::~PythonIntegrationPlugin() +{ + if (m_pluginModule) { + callPluginFunction("deinit"); + } + + // Acquire GIL for this plugin's interpreter + PyEval_RestoreThread(m_threadState); + + while (!m_runningTasks.isEmpty()) { + QFutureWatcher *watcher = m_runningTasks.keys().first(); + QString function = m_runningTasks.value(watcher); + + Py_BEGIN_ALLOW_THREADS + qCDebug(dcPythonIntegrations()) << "Waiting for" << metadata().pluginName() << "to finish" << function; + watcher->waitForFinished(); + Py_END_ALLOW_THREADS + } + + s_plugins.take(this); + Py_XDECREF(m_pluginModule); + Py_DECREF(m_nymeaModule); + + Py_EndInterpreter(m_threadState); + + PyThreadState_Swap(s_mainThreadState); + PyEval_ReleaseThread(s_mainThreadState); +} + +void PythonIntegrationPlugin::initPython() +{ + Q_ASSERT_X(s_mainThreadState == nullptr, "PythonIntegrationPlugin::initPython()", "initPython() must be called exactly once before calling deinitPython()."); + + // Only modify the init tab once (initPython() might be called again after calling deinitPython()) + static bool initTabPrepared = false; + if (!initTabPrepared) { + PyImport_AppendInittab("nymea", PyInit_nymea); + initTabPrepared = true; + } + + // Initialize the python engine and fire up threading support + Py_InitializeEx(0); + PyEval_InitThreads(); + + // Store the main thread state and release the GIL + s_mainThreadState = PyEval_SaveThread(); +} + +void PythonIntegrationPlugin::deinitPython() +{ + // Restore our main thread state + PyEval_RestoreThread(s_mainThreadState); + + // Tear down the python engine + Py_Finalize(); + + // Our main thread state is destroyed now + s_mainThreadState = nullptr; +} + +bool PythonIntegrationPlugin::loadScript(const QString &scriptFile) +{ + QFileInfo fi(scriptFile); + + QFile metaDataFile(fi.absolutePath() + "/" + fi.baseName() + ".json"); + if (!metaDataFile.open(QFile::ReadOnly)) { + qCWarning(dcPythonIntegrations()) << "Error opening metadata file:" << metaDataFile.fileName(); + return false; + } + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(metaDataFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(dcPythonIntegrations()) << "Error parsing metadata file:" << error.errorString(); + return false; + } + setMetaData(PluginMetadata(jsonDoc.object())); + if (!metadata().isValid()) { + qCWarning(dcPythonIntegrations()) << "Plugin metadata not valid for plugin:" << scriptFile; + return false; + } + + // Grab the main thread context and GIL + PyEval_RestoreThread(s_mainThreadState); + + // Create a new interpreter + m_threadState = Py_NewInterpreter(); + + // Switch to the new interpreter thread state + PyThreadState_Swap(m_threadState); + + // Import nymea module into this interpreter + m_nymeaModule = PyImport_ImportModule("nymea"); + + // Set up import path for the plugin directory + // We intentionally strip site-packages and dist-packages because + // that's too unpredictive in distribution. Instead all dependencies + // should be installed into the plugins "modules" subdir. + PyObject* sysPath = PySys_GetObject("path"); + QStringList importPaths; + for (int i = 0; i < PyList_Size(sysPath); i++) { + QString path = QString::fromUtf8(PyUnicode_AsUTF8(PyList_GetItem(sysPath, i))); + if (!path.contains("site-packages") && !path.contains("dist-packages")) { + importPaths.append(path); + } + } + importPaths.append(fi.absolutePath()); + importPaths.append(QString("%1/modules/").arg(fi.absolutePath())); + + PyObject* pluginPaths = PyList_New(importPaths.length()); + for (int i = 0; i < importPaths.length(); i++) { + const QString &path = importPaths.at(i); + PyObject *pyPath = PyUnicode_FromString(path.toUtf8()); + PyList_SetItem(pluginPaths, i, pyPath); + } + PySys_SetObject("path", pluginPaths); + + // Import the plugin + m_pluginModule = PyImport_ImportModule(fi.baseName().toUtf8()); + + if (!m_pluginModule) { + qCWarning(dcThingManager()) << "Error importing python plugin from:" << fi.absoluteFilePath(); + PyErr_Print(); + PyErr_Clear(); + PyEval_ReleaseThread(m_threadState); + return false; + } + qCDebug(dcThingManager()) << "Imported python plugin from" << fi.absoluteFilePath(); + + s_plugins.insert(this, m_pluginModule); + + // Set up logger with appropriate logging category + QString category = metadata().pluginName(); + category.replace(0, 1, category[0].toUpper()); + PyObject *args = Py_BuildValue("(s)", category.toUtf8().data()); + PyNymeaLoggingHandler *logger = reinterpret_cast(PyObject_CallObject((PyObject*)&PyNymeaLoggingHandlerType, args)); + Py_DECREF(args); + + // Override stdout and stderr + args = Py_BuildValue("(si)", category.toUtf8().data(), QtMsgType::QtDebugMsg); + PyStdOutHandler*stdOutHandler = reinterpret_cast(PyObject_CallObject((PyObject*)&PyStdOutHandlerType, args)); + Py_DECREF(args); + PySys_SetObject("stdout", (PyObject*)stdOutHandler); + args = Py_BuildValue("(si)", category.toUtf8().data(), QtMsgType::QtWarningMsg); + PyStdOutHandler*stdErrHandler = reinterpret_cast(PyObject_CallObject((PyObject*)&PyStdOutHandlerType, args)); + PySys_SetObject("stderr", (PyObject*)stdErrHandler); + Py_DECREF(args); + + int loggerAdded = PyModule_AddObject(m_pluginModule, "logger", reinterpret_cast(logger)); + if (loggerAdded != 0) { + qCWarning(dcPythonIntegrations()) << "Failed to add the logger object"; + Py_DECREF(logger); + } + + // Export metadata ids into module + exportIds(); + + // Register plugin api methods (plugin params etc) + PyModule_AddFunctions(m_pluginModule, plugin_methods); + + // As python does not have an event loop by default and uses blocking code a lot, we'll + // call every plugin method in a threaded way to prevent blocking the core while still not + // forcing every plugin developer to deal with threading in the plugin. + // In oder to not create and destroy a thread for each plugin api call, we'll be using a + // thread pool. + // The maximum number of threads in a plugin will be amount of things it manages + 2. + // This would allow for e.g. running an event loop using init(), performing something on a thing + // and still allow the user to perform a discovery at the same time. On the other hand, this is + // strict enough to not encourage the plugin developer to block forever in ever api call but use + // proper task processing means (timers, event loops etc) instead. + // Plugins can still spawn more threads on their own if the need to but have to manage them on their own. + m_threadPool = new QThreadPool(this); + m_threadPool->setMaxThreadCount(2); + qCDebug(dcPythonIntegrations()) << "Created a thread pool with a maximum of" << m_threadPool->maxThreadCount() << "threads for python plugin" << metadata().pluginName(); + + PyEval_ReleaseThread(m_threadState); + + // 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); + Py_DECREF(pyParamTypeId); + Py_DECREF(pyValue); + }); + + return true; +} + +void PythonIntegrationPlugin::init() +{ + m_mutex.lock(); + m_pluginConfigCopy = configuration(); + m_mutex.unlock(); + + callPluginFunction("init"); +} + +void PythonIntegrationPlugin::startMonitoringAutoThings() +{ + callPluginFunction("startMonitoringAutoThings"); +} + +void PythonIntegrationPlugin::discoverThings(ThingDiscoveryInfo *info) +{ + PyEval_RestoreThread(m_threadState); + + PyThingDiscoveryInfo *pyInfo = (PyThingDiscoveryInfo*)PyObject_CallObject((PyObject*)&PyThingDiscoveryInfoType, NULL); + PyThingDiscoveryInfo_setInfo(pyInfo, info); + + PyEval_ReleaseThread(m_threadState); + + connect(info, &ThingDiscoveryInfo::destroyed, this, [=](){ + PyEval_RestoreThread(m_threadState); + pyInfo->info = nullptr; + Py_DECREF(pyInfo); + PyEval_ReleaseThread(m_threadState); + }); + + callPluginFunction("discoverThings", reinterpret_cast(pyInfo)); +} + +void PythonIntegrationPlugin::startPairing(ThingPairingInfo *info) +{ + PyEval_RestoreThread(m_threadState); + + PyThingPairingInfo *pyInfo = (PyThingPairingInfo*)PyObject_CallObject((PyObject*)&PyThingPairingInfoType, nullptr); + PyThingPairingInfo_setInfo(pyInfo, info); + + PyEval_ReleaseThread(m_threadState); + + connect(info, &ThingPairingInfo::destroyed, this, [=](){ + PyEval_RestoreThread(m_threadState); + pyInfo->info = nullptr; + Py_DECREF(pyInfo); + PyEval_ReleaseThread(m_threadState); + }); + + bool result = callPluginFunction("startPairing", reinterpret_cast(pyInfo)); + if (!result) { + info->finish(Thing::ThingErrorHardwareFailure, "Plugin error: " + pluginName()); + } +} + +void PythonIntegrationPlugin::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + PyEval_RestoreThread(m_threadState); + + PyThingPairingInfo *pyInfo = (PyThingPairingInfo*)PyObject_CallObject((PyObject*)&PyThingPairingInfoType, nullptr); + PyThingPairingInfo_setInfo(pyInfo, info); + + PyEval_ReleaseThread(m_threadState); + + connect(info, &ThingPairingInfo::destroyed, this, [=](){ + PyEval_RestoreThread(m_threadState); + pyInfo->info = nullptr; + Py_DECREF(pyInfo); + PyEval_ReleaseThread(m_threadState); + }); + + PyObject *pyUsername = PyUnicode_FromString(username.toUtf8().data()); + PyObject *pySecret = PyUnicode_FromString(secret.toUtf8().data()); + bool result = callPluginFunction("confirmPairing", reinterpret_cast(pyInfo), pyUsername, pySecret); + if (!result) { + info->finish(Thing::ThingErrorHardwareFailure, "Plugin error: " + pluginName()); + } + + Py_DECREF(pyUsername); + Py_DECREF(pySecret); +} + +void PythonIntegrationPlugin::setupThing(ThingSetupInfo *info) +{ + PyEval_RestoreThread(m_threadState); + + Thing *thing = info->thing(); + + PyThing *pyThing = nullptr; + if (m_things.contains(thing)) { + pyThing = m_things.value(thing); + } else { + pyThing = (PyThing*)PyObject_CallObject((PyObject*)&PyThingType, NULL); + PyThing_setThing(pyThing, thing, m_threadState); + m_things.insert(thing, pyThing); + } + + PyObject *args = PyTuple_New(1); + PyTuple_SetItem(args, 0, (PyObject*)pyThing); + Py_INCREF(pyThing); + + PyThingSetupInfo *pyInfo = (PyThingSetupInfo*)PyObject_CallObject((PyObject*)&PyThingSetupInfoType, args); + Py_DECREF(args); + + pyInfo->info = info; + + m_threadPool->setMaxThreadCount(m_threadPool->maxThreadCount() + 1); + qCDebug(dcPythonIntegrations()) << "Expanded thread pool for plugin" << metadata().pluginName() << "to" << m_threadPool->maxThreadCount(); + + PyEval_ReleaseThread(m_threadState); + + connect(info->thing(), &Thing::destroyed, this, [=](){ + PyEval_RestoreThread(m_threadState); + m_things.remove(thing); + pyThing->thing = nullptr; + Py_DECREF(pyThing); + m_threadPool->setMaxThreadCount(m_threadPool->maxThreadCount() - 1); + qCDebug(dcPythonIntegrations()) << "Shrunk thread pool for plugin" << metadata().pluginName() << "to" << m_threadPool->maxThreadCount(); + PyEval_ReleaseThread(m_threadState); + }); + connect(info, &ThingSetupInfo::destroyed, this, [=](){ + PyEval_RestoreThread(m_threadState); + pyInfo->info = nullptr; + Py_DECREF(pyInfo); + PyEval_ReleaseThread(m_threadState); + }); + + + bool result = callPluginFunction("setupThing", reinterpret_cast(pyInfo)); + if (!result) { + // The python code did not even start, so let's finish (fail) the setup right away + info->finish(Thing::ThingErrorSetupFailed); + } +} + +void PythonIntegrationPlugin::postSetupThing(Thing *thing) +{ + PyThing* pyThing = m_things.value(thing); + callPluginFunction("postSetupThing", reinterpret_cast(pyThing)); +} + +void PythonIntegrationPlugin::executeAction(ThingActionInfo *info) +{ + PyThing *pyThing = m_things.value(info->thing()); + + PyEval_RestoreThread(m_threadState); + + PyThingActionInfo *pyInfo = (PyThingActionInfo*)PyObject_CallObject((PyObject*)&PyThingActionInfoType, NULL); + PyThingActionInfo_setInfo(pyInfo, info, pyThing); + + PyEval_ReleaseThread(m_threadState); + + connect(info, &ThingActionInfo::destroyed, this, [=](){ + qCDebug(dcPythonIntegrations()) << "Info destroyed"; + PyEval_RestoreThread(m_threadState); + qCDebug(dcPythonIntegrations()) << "Info destroyed2"; + pyInfo->info = nullptr; + Py_DECREF(pyInfo); + PyEval_ReleaseThread(m_threadState); + qCDebug(dcPythonIntegrations()) << "Info destroyed3"; + }); + + bool success = callPluginFunction("executeAction", reinterpret_cast(pyInfo)); + if (!success) { + info->finish(Thing::ThingErrorUnsupportedFeature); + } +} + +void PythonIntegrationPlugin::thingRemoved(Thing *thing) +{ + PyThing *pyThing = m_things.value(thing); + callPluginFunction("thingRemoved", reinterpret_cast(pyThing)); +} + +void PythonIntegrationPlugin::exportIds() +{ + qCDebug(dcThingManager()) << "Exporting plugin IDs:"; + QString pluginName = metadata().pluginName(); + QString pluginId = metadata().pluginId().toString(); + qCDebug(dcThingManager()) << "- Plugin:" << pluginName << pluginId; + PyModule_AddStringConstant(m_pluginModule, "pluginId", pluginId.toUtf8()); + + exportParamTypes(configurationDescription(), pluginName, "", "plugin"); + + foreach (const Vendor &vendor, supportedVendors()) { + qCDebug(dcThingManager()) << "|- Vendor:" << vendor.name() << vendor.id().toString(); + PyModule_AddStringConstant(m_pluginModule, QString("%1VendorId").arg(vendor.name()).toUtf8(), vendor.id().toString().toUtf8()); + } + + foreach (const ThingClass &thingClass, supportedThings()) { + exportThingClass(thingClass); + } +} + +void PythonIntegrationPlugin::exportThingClass(const ThingClass &thingClass) +{ + QString variableName = QString("%1ThingClassId").arg(thingClass.name()); + + qCDebug(dcThingManager()) << "|- ThingClass:" << variableName << thingClass.id().toString(); + PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), thingClass.id().toString().toUtf8()); + + exportParamTypes(thingClass.paramTypes(), thingClass.name(), "", "thing"); + exportParamTypes(thingClass.settingsTypes(), thingClass.name(), "", "settings"); + exportParamTypes(thingClass.discoveryParamTypes(), thingClass.name(), "", "discovery"); + + exportStateTypes(thingClass.stateTypes(), thingClass.name()); + exportEventTypes(thingClass.eventTypes(), thingClass.name()); + exportActionTypes(thingClass.actionTypes(), thingClass.name()); + exportBrowserItemActionTypes(thingClass.browserItemActionTypes(), thingClass.name()); +} + +void PythonIntegrationPlugin::exportParamTypes(const ParamTypes ¶mTypes, const QString &thingClassName, const QString &typeClass, const QString &typeName) +{ + 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 )); + qCDebug(dcThingManager()) << " |- ParamType:" << variableName << paramType.id().toString(); + PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), paramType.id().toString().toUtf8()); + } +} + +void PythonIntegrationPlugin::exportStateTypes(const StateTypes &stateTypes, const QString &thingClassName) +{ + foreach (const StateType &stateType, stateTypes) { + QString variableName = QString("%1%2StateTypeId").arg(thingClassName, stateType.name()[0].toUpper() + stateType.name().right(stateType.name().length() - 1)); + qCDebug(dcThingManager()) << " |- StateType:" << variableName << stateType.id().toString(); + PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), stateType.id().toString().toUtf8()); + } +} + +void PythonIntegrationPlugin::exportEventTypes(const EventTypes &eventTypes, const QString &thingClassName) +{ + foreach (const EventType &eventType, eventTypes) { + QString variableName = QString("%1%2EventTypeId").arg(thingClassName, eventType.name()[0].toUpper() + eventType.name().right(eventType.name().length() - 1)); + qCDebug(dcThingManager()) << " |- EventType:" << variableName << eventType.id().toString(); + PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), eventType.id().toString().toUtf8()); + exportParamTypes(eventType.paramTypes(), thingClassName, "Event", eventType.name()); + } +} + +void PythonIntegrationPlugin::exportActionTypes(const ActionTypes &actionTypes, const QString &thingClassName) +{ + foreach (const ActionType &actionType, actionTypes) { + QString variableName = QString("%1%2ActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1)); + qCDebug(dcThingManager()) << " |- ActionType:" << variableName << actionType.id().toString(); + PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), actionType.id().toString().toUtf8()); + exportParamTypes(actionType.paramTypes(), thingClassName, "Action", actionType.name()); + } +} + +void PythonIntegrationPlugin::exportBrowserItemActionTypes(const ActionTypes &actionTypes, const QString &thingClassName) +{ + foreach (const ActionType &actionType, actionTypes) { + QString variableName = QString("%1%2BrowserItemActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1)); + qCDebug(dcThingManager()) << " |- BrowserActionType:" << variableName << actionType.id().toString(); + PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), actionType.id().toString().toUtf8()); + exportParamTypes(actionType.paramTypes(), thingClassName, "BrowserItemAction", actionType.name()); + } +} + +bool PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObject *param1, PyObject *param2, PyObject *param3) +{ + PyEval_RestoreThread(m_threadState); + + qCDebug(dcThingManager()) << "Calling python plugin function" << function << "on plugin" << pluginName(); + PyObject *pluginFunction = PyObject_GetAttrString(m_pluginModule, function.toUtf8()); + if(!pluginFunction || !PyCallable_Check(pluginFunction)) { + PyErr_Clear(); + Py_XDECREF(pluginFunction); + qCDebug(dcThingManager()) << "Python plugin" << pluginName() << "does not implement" << function; + PyEval_ReleaseThread(m_threadState); + return false; + } + + Py_XINCREF(param1); + Py_XINCREF(param2); + Py_XINCREF(param3); + + QFutureWatcher *watcher = new QFutureWatcher(this); + + // Run the plugin function in the thread pool + QFuture future = QtConcurrent::run(m_threadPool, [=](){ + qCDebug(dcPythonIntegrations()) << "+++ Thread for" << function << "in plugin" << metadata().pluginName(); + + // Register this new thread in the interpreter + PyThreadState *threadState = PyThreadState_New(m_threadState->interp); + + // Acquire GIL and make the new thread state the current one + PyEval_RestoreThread(threadState); + + PyObject *pluginFunctionResult = PyObject_CallFunctionObjArgs(pluginFunction, param1, param2, param3, nullptr); + + if (PyErr_Occurred()) { + qCWarning(dcThingManager()) << "Error calling python method:" << function << "on plugin" << pluginName(); + PyErr_Print(); + } + + Py_DECREF(pluginFunction); + Py_XDECREF(pluginFunctionResult); + Py_XDECREF(param1); + Py_XDECREF(param2); + Py_XDECREF(param3); + + m_runningTasks.remove(watcher); + + // Destroy the thread and release the GIL + PyThreadState_Clear(threadState); + PyEval_ReleaseThread(threadState); + PyThreadState_Delete(threadState); + qCDebug(dcPythonIntegrations()) << "--- Thread for" << function << "in plugin" << metadata().pluginName(); + }); + watcher->setFuture(future); + m_runningTasks.insert(watcher, function); + + PyEval_ReleaseThread(m_threadState); + return true; +} + diff --git a/libnymea-core/integrations/pythonintegrationplugin.h b/libnymea-core/integrations/pythonintegrationplugin.h new file mode 100644 index 00000000..37bcaf28 --- /dev/null +++ b/libnymea-core/integrations/pythonintegrationplugin.h @@ -0,0 +1,94 @@ +#ifndef PYTHONINTEGRATIONPLUGIN_H +#define PYTHONINTEGRATIONPLUGIN_H + +#include "integrations/integrationplugin.h" + +#include +#include +#include +#include + +extern "C" { +typedef struct _object PyObject; +typedef struct _ts PyThreadState; +typedef struct _thing PyThing; +typedef struct _is PyInterpreterState; +} + + +class PythonIntegrationPlugin : public IntegrationPlugin +{ + Q_OBJECT +public: + explicit PythonIntegrationPlugin(QObject *parent = nullptr); + ~PythonIntegrationPlugin(); + + static void initPython(); + static void deinitPython(); + + bool loadScript(const QString &scriptFile); + + void init() override; + void startMonitoringAutoThings() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void startPairing(ThingPairingInfo *info) override; + void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + + + 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); + static PyObject* pyAutoThingDisappeared(PyObject *self, PyObject* args); + +private: + void exportIds(); + void exportThingClass(const ThingClass &thingClass); + void exportParamTypes(const ParamTypes ¶mTypes, const QString &thingClassName, const QString &typeClass, const QString &typeName); + void exportStateTypes(const StateTypes &stateTypes, const QString &thingClassName); + void exportEventTypes(const EventTypes &eventTypes, const QString &thingClassName); + void exportActionTypes(const ActionTypes &actionTypes, const QString &thingClassName); + void exportBrowserItemActionTypes(const ActionTypes &actionTypes, const QString &thingClassName); + + + bool callPluginFunction(const QString &function, PyObject *param1 = nullptr, PyObject *param2 = nullptr, PyObject *param3 = nullptr); + +private: + // The main thread state in which we create an interpreter per plugin + static PyThreadState* s_mainThreadState; + + // A per plugin thread state and interpreter + PyThreadState *m_threadState = nullptr; + + // A per plugin thread pool + QThreadPool *m_threadPool = nullptr; + + // Running concurrent tasks in this plugins thread pool + QHash*, QString> m_runningTasks; + + // The nymea module we import into the interpreter + PyObject *m_nymeaModule = nullptr; + // The imported plugin module (the plugin.py) + PyObject *m_pluginModule = nullptr; + + // 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; + + // Things held by this plugin instance + QHash m_things; + + // 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 1d13fd54..19a2e2ff 100644 --- a/libnymea-core/integrations/thingmanagerimplementation.cpp +++ b/libnymea-core/integrations/thingmanagerimplementation.cpp @@ -33,6 +33,7 @@ #if QT_VERSION >= QT_VERSION_CHECK(5,12,0) #include "scriptintegrationplugin.h" #endif +#include "pythonintegrationplugin.h" #include "loggingcategories.h" #include "typeutils.h" @@ -63,6 +64,7 @@ #include #include #include +#include ThingManagerImplementation::ThingManagerImplementation(HardwareManager *hardwareManager, const QLocale &locale, QObject *parent) : ThingManager(parent), @@ -70,9 +72,6 @@ ThingManagerImplementation::ThingManagerImplementation(HardwareManager *hardware m_locale(locale), m_translator(new Translator(this)) { - qRegisterMetaType(); - qRegisterMetaType(); - foreach (const Interface &interface, ThingUtils::allInterfaces()) { m_supportedInterfaces.insert(interface.name(), interface); } @@ -94,10 +93,13 @@ 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() { + delete m_translator; foreach (Thing *thing, m_configuredThings) { @@ -113,6 +115,8 @@ ThingManagerImplementation::~ThingManagerImplementation() qCDebug(dcThingManager()) << "Not deleting plugin" << plugin->pluginName(); } } + + PythonIntegrationPlugin::deinitPython(); } QStringList ThingManagerImplementation::pluginSearchDirs() @@ -139,32 +143,56 @@ QStringList ThingManagerImplementation::pluginSearchDirs() QList ThingManagerImplementation::pluginsMetadata() { QList pluginList; + 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 &entry, dir.entryList()) { - QFileInfo fi; + 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({"*.so", "*.js", "*.py"}, QDir::Files)) { + + 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"); + QPluginLoader loader(fi.absoluteFilePath()); + pluginList.append(loader.metaData().value("MetaData").toObject()); +#if QT_VERSION >= QT_VERSION_CHECK(5,12,0) + } else if (entry.startsWith("integrationplugin") && entry.endsWith(".js")) { + QFile jsonFile(fi.absolutePath() + "/" + fi.baseName() + ".json"); + if (!jsonFile.open(QFile::ReadOnly)) { + qCDebug(dcThingManager()) << "Failed to open json file for:" << entry; + continue; + } + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonFile.readAll()); + pluginList.append(jsonDoc.object()); +#endif + } else if (entry.startsWith("integrationplugin") && entry.endsWith(".py")) { + QFile jsonFile(fi.absolutePath() + "/" + fi.baseName() + ".json"); + if (!jsonFile.open(QFile::ReadOnly)) { + qCDebug(dcThingManager()) << "Failed to open json file for:" << entry; + continue; + } + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonFile.readAll()); + pluginList.append(jsonDoc.object()); } - if (!fi.exists()) { - continue; - } - QPluginLoader loader(fi.absoluteFilePath()); - pluginList.append(loader.metaData().value("MetaData").toObject()); } } return pluginList; } -void ThingManagerImplementation::registerStaticPlugin(IntegrationPlugin *plugin, const PluginMetadata &metaData) +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 @@ -1226,122 +1254,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; + ScriptIntegrationPlugin *p = new ScriptIntegrationPlugin(this); + bool ok = p->loadScript(fi.absoluteFilePath()); + if (ok) { + plugin = p; + } else { + delete p; } +#else + qCWarning(dcThingManager()) << "Not loading JS plugin as JS plugin support is not included in this nymea instance."; +#endif + } 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; } - loadPlugin(plugin, 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()); } } -#endif } -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()) { @@ -1994,7 +1973,7 @@ void ThingManagerImplementation::loadThingStates(Thing *thing) ThingClass thingClass = m_supportedThings.value(thing->thingClassId()); foreach (const StateType &stateType, thingClass.stateTypes()) { if (stateType.cached()) { - QVariant value(stateType.defaultValue()); + QVariant value = stateType.defaultValue(); if (settings.contains(stateType.id().toString())) { value = settings.value(stateType.id().toString()); @@ -2109,6 +2088,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 1842a5a6..f16a69a2 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -10,6 +10,23 @@ LIBS += -L$$top_builddir/libnymea/ -lnymea -lssl -lcrypto CONFIG += link_pkgconfig PKGCONFIG += nymea-mqtt nymea-networkmanager +# As of Ubuntu focal, there's a commonly named python3-embed pointing to the distro version of python +# For everything below python 3.8 we need to manually select one +packagesExist(python3-embed) { + PKGCONFIG += python3-embed +} else:packagesExist(python-3.5) { + # xenial, stretch + PKGCONFIG += python-3.5 +} else:packagesExist(python-3.6) { + # bionic + PKGCONFIG += python-3.6 +} else:packagesExist(python-3.7) { + # buster, eoan + PKGCONFIG += python-3.7 +} else { + error("Python development package not found.") +} + target.path = $$[QT_INSTALL_LIBS] INSTALLS += target @@ -20,8 +37,20 @@ RESOURCES += $$top_srcdir/icons.qrc \ HEADERS += nymeacore.h \ integrations/plugininfocache.h \ + integrations/python/pynymealogginghandler.h \ + integrations/python/pynymeamodule.h \ + integrations/python/pyparam.h \ + integrations/python/pystdouthandler.h \ + integrations/python/pything.h \ + integrations/python/pythingactioninfo.h \ + integrations/python/pythingdescriptor.h \ + integrations/python/pythingdiscoveryinfo.h \ + integrations/python/pythingpairinginfo.h \ + integrations/python/pythingsetupinfo.h \ + integrations/python/pyutils.h \ integrations/thingmanagerimplementation.h \ integrations/translator.h \ + integrations/pythonintegrationplugin.h \ experiences/experiencemanager.h \ ruleengine/ruleengine.h \ ruleengine/rule.h \ @@ -101,6 +130,7 @@ SOURCES += nymeacore.cpp \ integrations/plugininfocache.cpp \ integrations/thingmanagerimplementation.cpp \ integrations/translator.cpp \ + integrations/pythonintegrationplugin.cpp \ experiences/experiencemanager.cpp \ ruleengine/ruleengine.cpp \ ruleengine/rule.cpp \ 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 269a2b67..3f15c8c9 100644 --- a/libnymea/integrations/integrationplugin.cpp +++ b/libnymea/integrations/integrationplugin.cpp @@ -110,6 +110,11 @@ IntegrationPlugin::~IntegrationPlugin() } +PluginMetadata IntegrationPlugin::metadata() +{ + return m_metaData; +} + /*! Returns the name of this IntegrationPlugin. It returns the name value defined in the plugin's JSON file. */ QString IntegrationPlugin::pluginName() const { @@ -369,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 c525fd62..daeb4b06 100644 --- a/libnymea/integrations/integrationplugin.h +++ b/libnymea/integrations/integrationplugin.h @@ -79,6 +79,8 @@ public: IntegrationPlugin(QObject *parent = nullptr); virtual ~IntegrationPlugin(); + PluginMetadata metadata(); + virtual void init() {} PluginId pluginId() const; @@ -105,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; @@ -124,28 +126,19 @@ protected: HardwareManager *hardwareManager() const; QSettings *pluginStorage() const; + 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; - PluginMetadata m_metaData; ParamList m_config; }; Q_DECLARE_INTERFACE(IntegrationPlugin, "io.nymea.IntegrationPlugin") 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/libnymea/integrations/thing.h b/libnymea/integrations/thing.h index e8f03ab3..2e739aa0 100644 --- a/libnymea/integrations/thing.h +++ b/libnymea/integrations/thing.h @@ -107,8 +107,8 @@ public: ThingClass thingClass() const; - QString name() const; - void setName(const QString &name); + Q_INVOKABLE QString name() const; + Q_INVOKABLE void setName(const QString &name); ParamList params() const; bool hasParam(const ParamTypeId ¶mTypeId) const; @@ -117,12 +117,12 @@ public: QVariant paramValue(const ParamTypeId ¶mTypeId) const; void setParamValue(const ParamTypeId ¶mName, const QVariant &value); - ParamList settings() const; - bool hasSetting(const ParamTypeId ¶mTypeId) const; - void setSettings(const ParamList &settings); + Q_INVOKABLE ParamList settings() const; + Q_INVOKABLE bool hasSetting(const ParamTypeId ¶mTypeId) const; + Q_INVOKABLE void setSettings(const ParamList &settings); - QVariant setting(const ParamTypeId ¶mTypeId) const; - void setSettingValue(const ParamTypeId ¶mTypeId, const QVariant &value); + Q_INVOKABLE QVariant setting(const ParamTypeId ¶mTypeId) const; + Q_INVOKABLE void setSettingValue(const ParamTypeId ¶mTypeId, const QVariant &value); States states() const; bool hasState(const StateTypeId &stateTypeId) const; diff --git a/libnymea/integrations/thingdiscoveryinfo.h b/libnymea/integrations/thingdiscoveryinfo.h index d261881a..48d03b45 100644 --- a/libnymea/integrations/thingdiscoveryinfo.h +++ b/libnymea/integrations/thingdiscoveryinfo.h @@ -53,15 +53,15 @@ public: Thing::ThingError status() const; - void addThingDescriptor(const ThingDescriptor &thingDescriptor); - void addThingDescriptors(const ThingDescriptors &thingDescriptors); - ThingDescriptors thingDescriptors() const; QString displayMessage() const; QString translatedDisplayMessage(const QLocale &locale); public slots: + void addThingDescriptor(const ThingDescriptor &thingDescriptor); + void addThingDescriptors(const ThingDescriptors &thingDescriptors); + void finish(Thing::ThingError status, const QString &displayMessage = QString()); signals: diff --git a/libnymea/integrations/thingmanager.cpp b/libnymea/integrations/thingmanager.cpp index ae02ed1a..5053d861 100644 --- a/libnymea/integrations/thingmanager.cpp +++ b/libnymea/integrations/thingmanager.cpp @@ -47,8 +47,25 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent) { qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); } /*! Connect two states. diff --git a/libnymea/loggingcategories.h b/libnymea/loggingcategories.h index b2f8849d..dfb88212 100644 --- a/libnymea/loggingcategories.h +++ b/libnymea/loggingcategories.h @@ -89,6 +89,8 @@ Q_DECLARE_LOGGING_CATEGORY(dcMqtt) Q_DECLARE_LOGGING_CATEGORY(dcTranslations) Q_DECLARE_LOGGING_CATEGORY(dcCoap) Q_DECLARE_LOGGING_CATEGORY(dcI2C) +Q_DECLARE_LOGGING_CATEGORY(dcIntegrations) +Q_DECLARE_LOGGING_CATEGORY(dcJsIntegrations) /* diff --git a/libnymea/types/paramtype.cpp b/libnymea/types/paramtype.cpp index 13a6ca85..fd0758c4 100644 --- a/libnymea/types/paramtype.cpp +++ b/libnymea/types/paramtype.cpp @@ -272,7 +272,7 @@ void ParamTypes::put(const QVariant &variant) append(variant.value()); } -ParamType ParamTypes::findByName(const QString &name) +ParamType ParamTypes::findByName(const QString &name) const { foreach (const ParamType ¶mType, *this) { if (paramType.name() == name) { @@ -282,7 +282,7 @@ ParamType ParamTypes::findByName(const QString &name) return ParamType(); } -ParamType ParamTypes::findById(const ParamTypeId &id) +ParamType ParamTypes::findById(const ParamTypeId &id) const { foreach (const ParamType ¶mType, *this) { if (paramType.id() == id) { diff --git a/libnymea/types/paramtype.h b/libnymea/types/paramtype.h index ea4df364..8f8cede2 100644 --- a/libnymea/types/paramtype.h +++ b/libnymea/types/paramtype.h @@ -124,8 +124,8 @@ public: ParamTypes(const QList &other); Q_INVOKABLE QVariant get(int index) const; Q_INVOKABLE void put(const QVariant &variant); - ParamType findByName(const QString &name); - ParamType findById(const ParamTypeId &id); + ParamType findByName(const QString &name) const; + ParamType findById(const ParamTypeId &id) const; }; Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(ParamTypes) diff --git a/libnymea/types/state.cpp b/libnymea/types/state.cpp index 8366511f..8df85f32 100644 --- a/libnymea/types/state.cpp +++ b/libnymea/types/state.cpp @@ -117,3 +117,13 @@ void States::put(const QVariant &variant) { append(variant.value()); } + +QVariant States::stateValue(const StateTypeId &stateTypeId) +{ + foreach (const State & state, *this) { + if (state.stateTypeId() == stateTypeId) { + return state.value(); + } + } + return QVariant(); +} diff --git a/libnymea/types/state.h b/libnymea/types/state.h index 165c2fb9..f6b43659 100644 --- a/libnymea/types/state.h +++ b/libnymea/types/state.h @@ -69,6 +69,7 @@ public: States(const QList &other); Q_INVOKABLE QVariant get(int index) const; Q_INVOKABLE void put(const QVariant &variant); + Q_INVOKABLE QVariant stateValue(const StateTypeId &stateTypeId); }; Q_DECLARE_METATYPE(States) diff --git a/libnymea/types/statetype.h b/libnymea/types/statetype.h index f87e91eb..1532a434 100644 --- a/libnymea/types/statetype.h +++ b/libnymea/types/statetype.h @@ -103,7 +103,7 @@ private: QString m_name; QString m_displayName; int m_index = 0; - QVariant::Type m_type; + QVariant::Type m_type = QVariant::Invalid; QVariant m_defaultValue; QVariant m_minValue; QVariant m_maxValue; diff --git a/nymea.pro b/nymea.pro index 82cacc52..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}" @@ -105,4 +105,3 @@ coverage { ccache { message("Using ccache.") } - 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..9ec7c8e9 --- /dev/null +++ b/plugins/pymock/integrationpluginpymock.json @@ -0,0 +1,176 @@ +{ + "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", + "paramTypes": [ + { + "id": "8733557e-c599-4169-bcfa-5cc033c17b85", + "name": "param1", + "displayName": "Param 1", + "type": "bool", + "defaultValue": false + } + ], + "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", + "eventTypes": [ + { + "id": "de6c2425-0dee-413f-8f4c-bb0929e83c0d", + "name": "event1", + "displayName": "Event 1", + "paramTypes": [ + { + "id": "9f6aef52-dcde-4ee1-8ae3-4823594bf153", + "name": "param1", + "displayName": "Event param 1", + "type": "QString" + } + ] + } + ], + "stateTypes": [ + { + "id": "24714828-93ec-41a7-875e-6a0b5b57d25c", + "name": "state1", + "displayName": "State 1", + "displayNameEvent": "State 1 changed", + "displayNameAction": "Set state 1", + "type": "int", + "defaultValue": 0 + } + ], + "actionTypes": [ + { + "id": "a504933a-2b86-41c1-b188-8998d445adf8", + "name": "action1", + "displayName": "Action 1", + "paramTypes": [ + { + "id": "5a824a21-3e97-49a5-8b4d-2a5bc3ea99ef", + "name": "param1", + "displayName": "Action param 1", + "type": "QString", + "defaultValue": "hello" + } + ] + } + ] + }, + { + "id": "248c5046-847b-44d0-ab7c-684ff79197dc", + "name": "pyMockDiscoveryPairing", + "displayName": "Python mock thing with discovery and pairing", + "createMethods": ["discovery", "user"], + "setupMethod": "userAndPassword", + "discoveryParamTypes": [ + { + "id": "ef5f6b90-e9d8-4e77-a14d-6725cfb07116", + "name": "resultCount", + "displayName": "Result count", + "type": "uint", + "defaultValue": 2, + "minValue": 0, + "maxValue": 20 + } + ], + "paramTypes": [ + { + "id": "69328949-13ad-4bf4-b664-e7ee409ee51d", + "name": "param1", + "displayName": "Param 1", + "type": "int", + "defaultValue": 0 + } + ], + "settingsTypes": [ + { + "id": "d2312d00-22f5-4e2c-a6cc-8e4cade2d58b", + "name": "setting1", + "displayName": "Setting 1", + "type": "QString", + "defaultValue": "hello" + } + ], + "eventTypes": [ + { + "id": "e6b98ef6-7922-48e6-b508-238d178b86ca", + "name": "event1", + "displayName": "Event 1", + "paramTypes": [ + { + "id": "7c265a6a-f0ae-4822-a14f-e6a090f5a310", + "name": "param1", + "displayName": "Event param 1", + "type": "QString" + } + ] + } + ], + "stateTypes": [ + { + "id": "99d0af17-9e8c-42bb-bece-a5d114f051d3", + "name": "state1", + "displayName": "State 1", + "displayNameEvent": "State 1 changed", + "displayNameAction": "Set state 1", + "type": "int", + "defaultValue": 0 + } + ], + "actionTypes": [ + { + "id": "9bcc17ee-52a4-48ef-9a76-b2df4433dac5", + "name": "action1", + "displayName": "Action 1", + "paramTypes": [ + { + "id": "2e86cb76-e1a9-4afd-8fd3-e900cbb738f5", + "name": "param1", + "displayName": "Action param 1", + "type": "QString", + "defaultValue": "hello" + } + ] + } + ] + } + ] + } + ] +} diff --git a/plugins/pymock/integrationpluginpymock.py b/plugins/pymock/integrationpluginpymock.py new file mode 100644 index 00000000..fea164a3 --- /dev/null +++ b/plugins/pymock/integrationpluginpymock.py @@ -0,0 +1,144 @@ +import nymea +import time +#from fastdotcom import fast_com + +watchingAutoThings = False +loopRunning = False + +def init(): + global loopRunning + loopRunning = True + + logger.log("Python mock plugin init") + logger.warn("Python mock warning") + print("python stdout") + + while loopRunning: + time.sleep(5); + for thing in myThings(): + if thing.thingClassId == pyMockThingClassId: + logger.log("Emitting event 1 for", thing.name, "eventTypeId", pyMockEvent1EventTypeId) + thing.emitEvent(pyMockEvent1EventTypeId, [nymea.Param(pyMockEvent1EventParam1ParamTypeId, "Im an event")]) + logger.log("Setting state 1 for", thing.name, "to", thing.stateValue(pyMockState1StateTypeId) + 1) + thing.setStateValue(pyMockState1StateTypeId, thing.stateValue(pyMockState1StateTypeId) + 1) + if thing.thingClassId == pyMockDiscoveryPairingThingClassId: + logger.log("Emitting event 1 for", thing.name) + thing.emitEvent(pyMockDiscoveryPairingEvent1EventTypeId, [nymea.Param(pyMockDiscoveryPairingEvent1EventParam1ParamTypeId, "Im an event")]) + logger.log("Setting state 1 for", thing.name, "Old value is:", thing.stateValue(pyMockDiscoveryPairingState1StateTypeId)) + thing.setStateValue(pyMockDiscoveryPairingState1StateTypeId, thing.stateValue(pyMockDiscoveryPairingState1StateTypeId) + 1) + logger.log("Bye bye") + + +def deinit(): + logger.log("shutting down") + global loopRunning + loopRunning = False + + +def configValueChanged(paramTypeId, value): + logger.log("Plugin config value changed:", paramTypeId, value, watchingAutoThings) + if watchingAutoThings and paramTypeId == pyMockPluginAutoThingCountParamTypeId: + logger.log("Auto Thing Count plugin config changed:", value, "Currently there are:", len(autoThings()), "auto things") + things = autoThings(); + for i in range(len(things), value): + logger.log("Creating new auto thing") + descriptor = nymea.ThingDescriptor(pyMockAutoThingClassId, "Python Mock auto thing") + descriptor.params = [nymea.Param(pyMockAutoThingParam1ParamTypeId, True)] + autoThingsAppeared([descriptor]) + + for i in range(value, len(things)): + logger.log("Removing auto thing") + autoThingDisappeared(things[i].id) + + +def startMonitoringAutoThings(): + global watchingAutoThings + watchingAutoThings = True + logger.log("Start monitoring auto things. Have %i auto devices. Need %i." % (len(autoThings()), configValue(pyMockPluginAutoThingCountParamTypeId))) + things = autoThings(); + for i in range(len(things), configValue(pyMockPluginAutoThingCountParamTypeId)): + logger.log("Creating new auto thing") + descriptor = nymea.ThingDescriptor(pyMockAutoThingClassId, "Python Mock auto thing") + descriptor.params = [nymea.Param(pyMockAutoThingParam1ParamTypeId, True)] + autoThingsAppeared([descriptor]) + for i in range(configValue(pyMockPluginAutoThingCountParamTypeId), len(things)): + logger.log("Removing auto thing") + autoThingDisappeared(things[i].id) + + logger.log("Done start monitoring auto things") + + +def discoverThings(info): + logger.log("Discovery started for", info.thingClassId, "with result count:", info.params[0].value) + time.sleep(10) # Some delay for giving a feeling of a discovery + # Add 2 new discovery results + for i in range(0, info.params[0].value): + info.addDescriptor(nymea.ThingDescriptor(pyMockDiscoveryPairingThingClassId, "Python mock thing %i" % i)) + # Also add existing ones again so reconfiguration is possible + for thing in myThings(): + if thing.thingClassId == pyMockDiscoveryPairingThingClassId: + info.addDescriptor(nymea.ThingDescriptor(pyMockDiscoveryPairingThingClassId, thing.name, thingId=thing.id)) + + info.finish(nymea.ThingErrorNoError) + + +def startPairing(info): + logger.log("startPairing for", info.thingName, info.thingId, info.params) + info.finish(nymea.ThingErrorNoError, "Log in as user \"john\" with password \"smith\".") + + +def confirmPairing(info, username, secret): + logger.log("confirming pairing for", info.thingName, username, secret) + time.sleep(1) + if username == "john" and secret == "smith": + info.finish(nymea.ThingErrorNoError) + else: + info.finish(nymea.ThingErrorAuthenticationFailure, "Error logging in here!") + + +def setupThing(info): + logger.log("setupThing for", info.thing.name) + info.finish(nymea.ThingErrorNoError) + info.thing.nameChangedHandler = thingNameChanged + info.thing.settingChangedHandler = thingSettingChanged + + +def postSetupThing(thing): + logger.log("postSetupThing for", thing.name) + thing.nameChangedHandler = lambda thing : logger.log("Thing name changed", thing.name) + + if thing.thingClassId == pyMockAutoThingClassId: + logger.log("State 1 value:", thing.stateValue(pyMockAutoState1StateTypeId)) + + if thing.thingClassId == pyMockDiscoveryPairingThingClassId: + logger.log("Param 1 value:", thing.paramValue(pyMockDiscoveryPairingThingParam1ParamTypeId)) + logger.log("Setting 1 value:", thing.setting(pyMockDiscoveryPairingSettingsSetting1ParamTypeId)) + + +def executeAction(info): + logger.log("executeAction for", info.thing.name, info.actionTypeId, "with params", info.params) + paramValueByIndex = info.params[0].value + paramValueById = info.paramValue(pyMockAction1ActionParam1ParamTypeId) + logger.log("Param by index:", paramValueByIndex, "by ID:", paramValueById) + info.finish(nymea.ThingErrorNoError) + + +def autoThings(): + autoThings = [] + for thing in myThings(): + if thing.thingClassId == pyMockAutoThingClassId: + autoThings.append(thing) + return autoThings + + +def thingNameChanged(thing, name): + logger.log("Thing name changed:", thing.name) + + +def thingSettingChanged(thing, paramTypeId, value): + logger.log("Thing setting changed:", thing.name, paramTypeId, value) + + +# Intentionally commented out to also have a test case for unimplmented functions +# def thingRemoved(thing): +# logger.log("thingRemoved for", thing.name) diff --git a/plugins/pymock/pymock.pro b/plugins/pymock/pymock.pro new file mode 100644 index 00000000..4eb1428c --- /dev/null +++ b/plugins/pymock/pymock.pro @@ -0,0 +1,12 @@ +TEMPLATE = aux + +OTHER_FILES = integrationpluginpymock.json \ + integrationpluginpymock.py + + +# Copy files to build dir as we've set plugin import paths to that +copydata.commands = $(COPY_DIR) $$PWD/integrationpluginpymock.json $$PWD/*.py $$OUT_PWD || true +first.depends = $(first) copydata +export(first.depends) +export(copydata.commands) +QMAKE_EXTRA_TARGETS += first copydata diff --git a/python-todos.txt b/python-todos.txt new file mode 100644 index 00000000..59fce1f5 --- /dev/null +++ b/python-todos.txt @@ -0,0 +1,2 @@ + +* pluginStorage missing diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index c6431395..db10112b 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -13,6 +13,7 @@ SUBDIRS = \ loggingloading \ mqttbroker \ plugins \ + pythonplugins \ rules \ scripts \ states \ diff --git a/tests/auto/integrations/testintegrations.cpp b/tests/auto/integrations/testintegrations.cpp index b3c6e373..bfacf23b 100644 --- a/tests/auto/integrations/testintegrations.cpp +++ b/tests/auto/integrations/testintegrations.cpp @@ -161,6 +161,7 @@ void TestIntegrations::initTestCase() "Tests.debug=true\n" "Mock.debug=true\n" "Translations.debug=true\n" + "PythonIntegrations.debug=true\n" ); // Adding an async mock to be used in tests below @@ -290,8 +291,8 @@ void TestIntegrations::getThingClasses_data() QTest::addColumn("vendorId"); QTest::addColumn("resultCount"); - QTest::newRow("vendor nymea") << nymeaVendorId << 14; - QTest::newRow("no filter") << VendorId() << 14; + QTest::newRow("vendor nymea") << nymeaVendorId << 17; + QTest::newRow("no filter") << VendorId() << 17; QTest::newRow("invalid vendor") << VendorId("93e7d361-8025-4354-b17e-b68406c800bc") << 0; } diff --git a/tests/auto/pythonplugins/pythonplugins.pro b/tests/auto/pythonplugins/pythonplugins.pro new file mode 100644 index 00000000..36d4b792 --- /dev/null +++ b/tests/auto/pythonplugins/pythonplugins.pro @@ -0,0 +1,6 @@ +TARGET = testpythonplugins + +include(../../../nymea.pri) +include(../autotests.pri) + +SOURCES += testpythonplugins.cpp diff --git a/tests/auto/pythonplugins/testpythonplugins.cpp b/tests/auto/pythonplugins/testpythonplugins.cpp new file mode 100644 index 00000000..c15dd764 --- /dev/null +++ b/tests/auto/pythonplugins/testpythonplugins.cpp @@ -0,0 +1,141 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "nymeatestbase.h" + +#include "integrations/thing.h" + +ThingClassId pyMockThingClassId = ThingClassId("1761c256-99b1-41bd-988a-a76087f6a4f1"); +ThingClassId pyMockDiscoveryPairingThingClassId = ThingClassId("248c5046-847b-44d0-ab7c-684ff79197dc"); +ParamTypeId pyMockDiscoveryPairingResultCountDiscoveryParamTypeID = ParamTypeId("ef5f6b90-e9d8-4e77-a14d-6725cfb07116"); + +using namespace nymeaserver; + +class TestPythonPlugins: public NymeaTestBase +{ + Q_OBJECT + +private: + inline void verifyThingError(const QVariant &response, Thing::ThingError error = Thing::ThingErrorNoError) { + verifyError(response, "thingError", enumValueName(error)); + } + +private slots: + + void initTestCase(); + + void setupAndRemoveThing(); + void testDiscoverPairAndRemoveThing(); + + +}; + + +void TestPythonPlugins::initTestCase() +{ + NymeaTestBase::initTestCase(); + QLoggingCategory::setFilterRules("*.debug=false\n" + "Tests.debug=true\n" + "PyMock.debug=true\n" + "PythonIntegrations.debug=true\n" + ); +} + +void TestPythonPlugins::setupAndRemoveThing() +{ + QVariantMap resultCountParam; + resultCountParam.insert("paramTypeId", pyMockDiscoveryPairingResultCountDiscoveryParamTypeID); + resultCountParam.insert("value", 2); + + QVariantList discoveryParams; + discoveryParams.append(resultCountParam); + + QVariantMap params; + params.insert("thingClassId", pyMockThingClassId); + params.insert("name", "Py test thing"); + QVariant response = injectAndWait("Integrations.AddThing", params); + + verifyThingError(response, Thing::ThingErrorNoError); + ThingId thingId = response.toMap().value("params").toMap().value("thingId").toUuid(); + qCDebug(dcTests()) << "New thing id" << thingId; + + params.clear(); + params.insert("thingId", thingId); + injectAndWait("Integrations.RemoveThing", params); + verifyThingError(response, Thing::ThingErrorNoError); +} + +void TestPythonPlugins::testDiscoverPairAndRemoveThing() +{ + // Discover + QVariantMap resultCountParam; + resultCountParam.insert("paramTypeId", pyMockDiscoveryPairingResultCountDiscoveryParamTypeID); + resultCountParam.insert("value", 2); + + QVariantList discoveryParams; + discoveryParams.append(resultCountParam); + + QVariantMap params; + params.insert("thingClassId", pyMockDiscoveryPairingThingClassId); + params.insert("discoveryParams", discoveryParams); + QVariant response = injectAndWait("Integrations.DiscoverThings", params); + + verifyThingError(response, Thing::ThingErrorNoError); + QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), 2); + + ThingDescriptorId descriptorId = response.toMap().value("params").toMap().value("thingDescriptors").toList().first().toMap().value("id").toUuid(); + + // Pair + params.clear(); + params.insert("thingDescriptorId", descriptorId); + response = injectAndWait("Integrations.PairThing", params); + verifyThingError(response, Thing::ThingErrorNoError); + + qWarning() << "respo" << response.toMap().value("params").toMap(); + PairingTransactionId transactionId = response.toMap().value("params").toMap().value("pairingTransactionId").toUuid(); + qWarning() << "transactionId" << transactionId; + + params.clear(); + params.insert("pairingTransactionId", transactionId); + params.insert("username", "john"); + params.insert("secret", "smith"); + response = injectAndWait("Integrations.ConfirmPairing", params); + verifyThingError(response, Thing::ThingErrorNoError); + ThingId thingId = response.toMap().value("params").toMap().value("thingId").toUuid(); + + // Remove + params.clear(); + params.insert("thingId", thingId); + response = injectAndWait("Integrations.RemoveThing", params); + verifyThingError(response, Thing::ThingErrorNoError); +} + +#include "testpythonplugins.moc" +QTEST_MAIN(TestPythonPlugins) diff --git a/tests/testlib/nymeatestbase.cpp b/tests/testlib/nymeatestbase.cpp index d124b865..cdc687f6 100644 --- a/tests/testlib/nymeatestbase.cpp +++ b/tests/testlib/nymeatestbase.cpp @@ -414,7 +414,9 @@ void NymeaTestBase::waitForDBSync() void NymeaTestBase::restartServer() { // Destroy and recreate the core instance... + qCDebug(dcTests()) << "Tearing down server instance"; NymeaCore::instance()->destroy(); + qCDebug(dcTests()) << "Restarting server instance"; NymeaCore::instance()->init(); QSignalSpy coreSpy(NymeaCore::instance(), SIGNAL(initialized())); coreSpy.wait();