mirror of https://github.com/nymea/nymea.git
Merge PR #310: Add support for Python plugins
commit
694b070f05
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
#ifndef PYNYMEALOGGINGHANDLER_H
|
||||
#define PYNYMEALOGGINGHANDLER_H
|
||||
|
||||
#include <Python.h>
|
||||
#include "structmember.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
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<initproc>(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
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#ifndef PYNYMEAMODULE_H
|
||||
#define PYNYMEAMODULE_H
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#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
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
#ifndef PYPARAM_H
|
||||
#define PYPARAM_H
|
||||
|
||||
#include <Python.h>
|
||||
#include <structmember.h>
|
||||
|
||||
#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<PyParam*>(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<initproc>(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<PyObject*>(&PyParamType));
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif // PYPARAM_H
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
#ifndef PYSTDOUTHANDLER_H
|
||||
#define PYSTDOUTHANDLER_H
|
||||
|
||||
#include <Python.h>
|
||||
#include "structmember.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
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<initproc>(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
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
#ifndef PYTHING_H
|
||||
#define PYTHING_H
|
||||
|
||||
#include <Python.h>
|
||||
#include "structmember.h"
|
||||
|
||||
#include "pyparam.h"
|
||||
|
||||
#include "integrations/thing.h"
|
||||
#include "loggingcategories.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QThread>
|
||||
#include <QMetaEnum>
|
||||
|
||||
#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<PyObject*>(&PyThingType));
|
||||
|
||||
QMetaEnum thingErrorEnum = QMetaEnum::fromType<Thing::ThingError>();
|
||||
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
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
#ifndef PYTHINGACTIONINFO_H
|
||||
#define PYTHINGACTIONINFO_H
|
||||
|
||||
#include <Python.h>
|
||||
#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<Thing::ThingError>(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<PyParam*>(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
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
#ifndef PYTHINGDESCRIPTOR_H
|
||||
#define PYTHINGDESCRIPTOR_H
|
||||
|
||||
#include <Python.h>
|
||||
#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<initproc>(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<PyObject*>(&PyThingDescriptorType));
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif // PYTHINGDESCRIPTOR_H
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
#ifndef PYTHINGDISCOVERYINFO_H
|
||||
#define PYTHINGDISCOVERYINFO_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
#include "structmember.h"
|
||||
|
||||
#include "pythingdescriptor.h"
|
||||
#include "pyparam.h"
|
||||
|
||||
#include "integrations/thingdiscoveryinfo.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QMetaEnum>
|
||||
#include <QMutex>
|
||||
|
||||
#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<Thing::ThingError>(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
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
#ifndef PYTHINGPAIRINGINFO_H
|
||||
#define PYTHINGPAIRINGINFO_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
#include "structmember.h"
|
||||
|
||||
#include "pyparam.h"
|
||||
|
||||
#include "integrations/thingpairinginfo.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QMetaEnum>
|
||||
#include <QMutex>
|
||||
|
||||
#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<Thing::ThingError>(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
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
#ifndef PYTHINGSETUPINFO_H
|
||||
#define PYTHINGSETUPINFO_H
|
||||
|
||||
#include <Python.h>
|
||||
#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<Thing::ThingError>(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
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#ifndef PYUTILS_H
|
||||
#define PYUTILS_H
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QVariant>
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,705 @@
|
|||
#include <Python.h>
|
||||
|
||||
#include "pythonintegrationplugin.h"
|
||||
#include "python/pynymeamodule.h"
|
||||
#include "python/pystdouthandler.h"
|
||||
|
||||
#include "loggingcategories.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QMetaEnum>
|
||||
#include <QJsonDocument>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#include <QCoreApplication>
|
||||
#include <QMutex>
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
|
||||
NYMEA_LOGGING_CATEGORY(dcPythonIntegrations, "PythonIntegrations")
|
||||
|
||||
PyThreadState* PythonIntegrationPlugin::s_mainThreadState = nullptr;
|
||||
QHash<PythonIntegrationPlugin*, PyObject*> 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<void> *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<PyNymeaLoggingHandler*>(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<PyStdOutHandler*>(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<PyStdOutHandler*>(PyObject_CallObject((PyObject*)&PyStdOutHandlerType, args));
|
||||
PySys_SetObject("stderr", (PyObject*)stdErrHandler);
|
||||
Py_DECREF(args);
|
||||
|
||||
int loggerAdded = PyModule_AddObject(m_pluginModule, "logger", reinterpret_cast<PyObject*>(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<PyObject*>(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<PyObject*>(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<PyObject*>(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<PyObject*>(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<PyObject*>(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<PyObject*>(pyInfo));
|
||||
if (!success) {
|
||||
info->finish(Thing::ThingErrorUnsupportedFeature);
|
||||
}
|
||||
}
|
||||
|
||||
void PythonIntegrationPlugin::thingRemoved(Thing *thing)
|
||||
{
|
||||
PyThing *pyThing = m_things.value(thing);
|
||||
callPluginFunction("thingRemoved", reinterpret_cast<PyObject*>(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<void> *watcher = new QFutureWatcher<void>(this);
|
||||
|
||||
// Run the plugin function in the thread pool
|
||||
QFuture<void> 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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
#ifndef PYTHONINTEGRATIONPLUGIN_H
|
||||
#define PYTHONINTEGRATIONPLUGIN_H
|
||||
|
||||
#include "integrations/integrationplugin.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
#include <QFuture>
|
||||
#include <QThreadPool>
|
||||
|
||||
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<QFutureWatcher<void>*, 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<PythonIntegrationPlugin*, PyObject*> s_plugins;
|
||||
|
||||
// Used for guarding access from the python threads to the plugin instance
|
||||
QMutex m_mutex;
|
||||
|
||||
// Things held by this plugin instance
|
||||
QHash<Thing*, PyThing*> m_things;
|
||||
|
||||
// Need to keep a copy of plugin params and sync that in a thread-safe manner
|
||||
ParamList m_pluginConfigCopy;
|
||||
|
||||
};
|
||||
|
||||
#endif // PYTHONINTEGRATIONPLUGIN_H
|
||||
|
|
@ -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 <QCoreApplication>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QJsonDocument>
|
||||
|
||||
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<ThingClassId>();
|
||||
qRegisterMetaType<ThingDescriptor>();
|
||||
|
||||
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<QJsonObject> ThingManagerImplementation::pluginsMetadata()
|
||||
{
|
||||
QList<QJsonObject> 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<QString(*)()>(versionFunc)();
|
||||
lib.unload();
|
||||
QStringList parts = version.split('.');
|
||||
QStringList coreParts = QString(LIBNYMEA_API_VERSION).split('.');
|
||||
if (parts.length() != 3 || parts.at(0).toInt() != coreParts.at(0).toInt() || parts.at(1).toInt() > coreParts.at(1).toInt()) {
|
||||
qCWarning(dcThingManager()).nospace() << "Libnymea API mismatch for " << fi.absoluteFilePath() << ". Core API: " << LIBNYMEA_API_VERSION << ", Plugin API: " << version;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Version is ok. Now load the plugin
|
||||
QPluginLoader loader;
|
||||
loader.setFileName(fi.absoluteFilePath());
|
||||
loader.setLoadHints(QLibrary::ResolveAllSymbolsHint);
|
||||
|
||||
qCDebug(dcThingManager()) << "Loading plugin from:" << fi.absoluteFilePath();
|
||||
if (!loader.load()) {
|
||||
qCWarning(dcThingManager) << "Could not load plugin data of" << entry << "\n" << loader.errorString();
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject pluginInfo = loader.metaData().value("MetaData").toObject();
|
||||
PluginMetadata metaData(pluginInfo, false, false);
|
||||
if (!metaData.isValid()) {
|
||||
foreach (const QString &error, metaData.validationErrors()) {
|
||||
qCWarning(dcThingManager()) << error;
|
||||
}
|
||||
loader.unload();
|
||||
continue;
|
||||
}
|
||||
|
||||
IntegrationPlugin *pluginIface = qobject_cast<IntegrationPlugin *>(loader.instance());
|
||||
if (!pluginIface) {
|
||||
qCWarning(dcThingManager) << "Could not get plugin instance of" << entry;
|
||||
loader.unload();
|
||||
continue;
|
||||
}
|
||||
if (m_integrationPlugins.contains(pluginIface->pluginId())) {
|
||||
qCWarning(dcThingManager()) << "A plugin with this ID is already loaded. Not loading" << entry;
|
||||
continue;
|
||||
}
|
||||
loadPlugin(pluginIface, metaData);
|
||||
PluginInfoCache::cachePluginInfo(pluginInfo);
|
||||
}
|
||||
}
|
||||
plugin = createCppIntegrationPlugin(fi.absoluteFilePath());
|
||||
|
||||
} else if (entry.startsWith("integrationplugin") && entry.endsWith(".js")) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5,12,0)
|
||||
foreach (const QString &path, pluginSearchDirs()) {
|
||||
QDir dir(path);
|
||||
qCDebug(dcThingManager) << "Loading JS plugins from:" << dir.absolutePath();
|
||||
foreach (const QString &entry, dir.entryList()) {
|
||||
QFileInfo jsFi;
|
||||
QFileInfo jsonFi;
|
||||
|
||||
if (entry.endsWith(".js")) {
|
||||
jsFi.setFile(path + "/" + entry);
|
||||
} else {
|
||||
jsFi.setFile(path + "/" + entry + "/" + entry + ".js");
|
||||
}
|
||||
|
||||
if (!jsFi.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ScriptIntegrationPlugin *plugin = new ScriptIntegrationPlugin(this);
|
||||
bool ret = plugin->loadScript(jsFi.absoluteFilePath());
|
||||
if (!ret) {
|
||||
delete plugin;
|
||||
qCWarning(dcThingManager()) << "JS plugin failed to load";
|
||||
continue;
|
||||
}
|
||||
PluginMetadata metaData(plugin->metaData());
|
||||
if (!metaData.isValid()) {
|
||||
qCWarning(dcThingManager()) << "Not loading JS plugin. Invalid metadata.";
|
||||
foreach (const QString &error, metaData.validationErrors()) {
|
||||
qCWarning(dcThingManager()) << error;
|
||||
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<QString(*)()>(versionFunc)();
|
||||
lib.unload();
|
||||
QStringList parts = version.split('.');
|
||||
QStringList coreParts = QString(LIBNYMEA_API_VERSION).split('.');
|
||||
if (parts.length() != 3 || parts.at(0).toInt() != coreParts.at(0).toInt() || parts.at(1).toInt() > coreParts.at(1).toInt()) {
|
||||
qCWarning(dcThingManager()).nospace() << "Libnymea API mismatch for " << absoluteFilePath << ". Core API: " << LIBNYMEA_API_VERSION << ", Plugin API: " << version;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Version is ok. Now load the plugin
|
||||
QPluginLoader loader;
|
||||
loader.setFileName(absoluteFilePath);
|
||||
loader.setLoadHints(QLibrary::ResolveAllSymbolsHint);
|
||||
|
||||
qCDebug(dcThingManager()) << "Loading plugin from:" << absoluteFilePath;
|
||||
if (!loader.load()) {
|
||||
qCWarning(dcThingManager) << "Could not load plugin data of" << absoluteFilePath << "\n" << loader.errorString();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QJsonObject pluginInfo = loader.metaData().value("MetaData").toObject();
|
||||
PluginMetadata metaData(pluginInfo, false, false);
|
||||
if (!metaData.isValid()) {
|
||||
foreach (const QString &error, metaData.validationErrors()) {
|
||||
qCWarning(dcThingManager()) << error;
|
||||
}
|
||||
loader.unload();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QObject *p = loader.instance();
|
||||
if (!p) {
|
||||
qCWarning(dcThingManager()) << "Error loading plugin:" << loader.errorString();
|
||||
return nullptr;
|
||||
}
|
||||
IntegrationPlugin *pluginIface = qobject_cast<IntegrationPlugin *>(p);
|
||||
if (!pluginIface) {
|
||||
qCWarning(dcThingManager) << "Could not get plugin instance of" << absoluteFilePath;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pluginIface->setMetaData(PluginMetadata(pluginInfo));
|
||||
|
||||
return pluginIface;
|
||||
}
|
||||
|
||||
void ThingManagerImplementation::storeThingStates(Thing *thing)
|
||||
{
|
||||
ThingClass thingClass = m_supportedThings.value(thing->thingClassId());
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public:
|
|||
|
||||
static QStringList pluginSearchDirs();
|
||||
static QList<QJsonObject> pluginsMetadata();
|
||||
void registerStaticPlugin(IntegrationPlugin* plugin, const PluginMetadata &metaData);
|
||||
void registerStaticPlugin(IntegrationPlugin* plugin);
|
||||
|
||||
IntegrationPlugins plugins() const override;
|
||||
IntegrationPlugin *plugin(const PluginId &pluginId) const override;
|
||||
|
|
@ -131,7 +131,7 @@ signals:
|
|||
|
||||
private slots:
|
||||
void loadPlugins();
|
||||
void loadPlugin(IntegrationPlugin *pluginIface, const PluginMetadata &metaData);
|
||||
void loadPlugin(IntegrationPlugin *pluginIface);
|
||||
void loadConfiguredThings();
|
||||
void storeConfiguredThings();
|
||||
void startMonitoringAutoThings();
|
||||
|
|
@ -165,6 +165,8 @@ private:
|
|||
void syncIOConnection(Thing *inputThing, const StateTypeId &stateTypeId);
|
||||
QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const;
|
||||
|
||||
IntegrationPlugin *createCppIntegrationPlugin(const QString &absoluteFilePath);
|
||||
|
||||
private:
|
||||
HardwareManager *m_hardwareManager;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<bool, QList<ParamType> > parseParamTypes(const QJsonArray &array) const;
|
||||
|
||||
// Returns <missingFields, unknownFields>
|
||||
QPair<QStringList, QStringList> verifyFields(const QStringList &possibleFields, const QStringList &mandatoryFields, const QJsonObject &value) const;
|
||||
|
||||
// load and verify enum values
|
||||
QPair<bool, Types::Unit> loadAndVerifyUnit(const QString &unitString) const;
|
||||
QPair<bool, Types::InputType> loadAndVerifyInputType(const QString &inputType) const;
|
||||
|
||||
PluginMetadata m_metaData;
|
||||
ThingManager *m_thingManager = nullptr;
|
||||
HardwareManager *m_hardwareManager = nullptr;
|
||||
QSettings *m_storage = nullptr;
|
||||
|
||||
PluginMetadata m_metaData;
|
||||
ParamList m_config;
|
||||
};
|
||||
Q_DECLARE_INTERFACE(IntegrationPlugin, "io.nymea.IntegrationPlugin")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
#include "types/paramtype.h"
|
||||
#include "types/thingclass.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
class PluginMetadata
|
||||
{
|
||||
public:
|
||||
|
|
@ -53,6 +55,7 @@ public:
|
|||
Vendors vendors() const;
|
||||
ThingClasses thingClasses() const;
|
||||
|
||||
QJsonObject jsonObject() const;
|
||||
|
||||
private:
|
||||
void parse(const QJsonObject &jsonObject);
|
||||
|
|
@ -64,6 +67,7 @@ private:
|
|||
bool verifyDuplicateUuid(const QUuid &uuid);
|
||||
|
||||
private:
|
||||
QJsonObject m_jsonObject;
|
||||
bool m_isValid = false;
|
||||
bool m_isBuiltIn = false;
|
||||
PluginId m_pluginId;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -47,8 +47,25 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent)
|
|||
{
|
||||
qRegisterMetaType<Param>();
|
||||
qRegisterMetaType<ParamList>();
|
||||
qRegisterMetaType<ParamTypeId>();
|
||||
qRegisterMetaType<ParamType>();
|
||||
qRegisterMetaType<ParamTypes>();
|
||||
qRegisterMetaType<StateTypeId>();
|
||||
qRegisterMetaType<StateType>();
|
||||
qRegisterMetaType<StateTypes>();
|
||||
qRegisterMetaType<EventTypeId>();
|
||||
qRegisterMetaType<EventType>();
|
||||
qRegisterMetaType<EventTypes>();
|
||||
qRegisterMetaType<ActionTypeId>();
|
||||
qRegisterMetaType<ActionType>();
|
||||
qRegisterMetaType<ActionTypes>();
|
||||
qRegisterMetaType<ThingClassId>();
|
||||
qRegisterMetaType<ThingClass>();
|
||||
qRegisterMetaType<ThingClasses>();
|
||||
qRegisterMetaType<ThingDescriptorId>();
|
||||
qRegisterMetaType<ThingDescriptor>();
|
||||
qRegisterMetaType<ThingDescriptors>();
|
||||
qRegisterMetaType<Thing::ThingError>();
|
||||
}
|
||||
|
||||
/*! Connect two states.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ void ParamTypes::put(const QVariant &variant)
|
|||
append(variant.value<ParamType>());
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -124,8 +124,8 @@ public:
|
|||
ParamTypes(const QList<ParamType> &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<ParamType>)
|
||||
Q_DECLARE_METATYPE(ParamTypes)
|
||||
|
|
|
|||
|
|
@ -117,3 +117,13 @@ void States::put(const QVariant &variant)
|
|||
{
|
||||
append(variant.value<State>());
|
||||
}
|
||||
|
||||
QVariant States::stateValue(const StateTypeId &stateTypeId)
|
||||
{
|
||||
foreach (const State & state, *this) {
|
||||
if (state.stateTypeId() == stateTypeId) {
|
||||
return state.value();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public:
|
|||
States(const QList<State> &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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ QT+= network
|
|||
|
||||
TARGET = $$qtLibraryTarget(nymea_integrationpluginmock)
|
||||
|
||||
OTHER_FILES += interationpluginmock.json
|
||||
OTHER_FILES += integrationpluginmock.json
|
||||
|
||||
SOURCES += \
|
||||
integrationpluginmock.cpp \
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
|
||||
extern "C" const QString libnymea_api_version() { return QString("6.0.0");}
|
||||
extern "C" const QString libnymea_api_version() { return QString("7.0.0");}
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcMock)
|
||||
Q_LOGGING_CATEGORY(dcMock, "Mock")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
TEMPLATE = subdirs
|
||||
|
||||
!disabletesting: {
|
||||
SUBDIRS += mock
|
||||
SUBDIRS += mock pymock
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
import builtins
|
||||
|
||||
builtins.pyMockPluginAutoThingCountParamTypeId = "{1d3422cb-fcdd-4ab5-ac6e-056288439343}"
|
||||
builtins.foo = "bar"
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
* pluginStorage missing
|
||||
|
|
@ -13,6 +13,7 @@ SUBDIRS = \
|
|||
loggingloading \
|
||||
mqttbroker \
|
||||
plugins \
|
||||
pythonplugins \
|
||||
rules \
|
||||
scripts \
|
||||
states \
|
||||
|
|
|
|||
|
|
@ -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>("vendorId");
|
||||
QTest::addColumn<int>("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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
TARGET = testpythonplugins
|
||||
|
||||
include(../../../nymea.pri)
|
||||
include(../autotests.pri)
|
||||
|
||||
SOURCES += 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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)
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue