Merge PR #310: Add support for Python plugins

pull/352/head
Jenkins nymea 2020-10-28 19:07:25 +01:00
commit 694b070f05
46 changed files with 3172 additions and 168 deletions

1
debian/control vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -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, &paramTypeId, &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 &param)
{
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 &params)
{
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

View File

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

View File

@ -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 &paramTypeId, 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", &paramTypeIdStr)) {
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", &paramTypeIdStr)) {
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

View File

@ -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", &paramTypeIdStr)) {
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

View File

@ -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, &params))
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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", &paramTypeIdStr)) {
qCWarning(dcThingManager) << "Error parsing parameters";
return nullptr;
}
ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr);
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
plugin->m_mutex.lock();
QVariant value = plugin->m_pluginConfigCopy.paramValue(paramTypeId);
plugin->m_mutex.unlock();
return QVariantToPyObject(value);
}
PyObject *PythonIntegrationPlugin::pySetConfigValue(PyObject *self, PyObject *args)
{
char *paramTypeIdStr = nullptr;
PyObject *valueObj = nullptr;
if (!PyArg_ParseTuple(args, "sO", &paramTypeIdStr, &valueObj)) {
qCWarning(dcThingManager) << "Error parsing parameters";
return nullptr;
}
ParamTypeId paramTypeId = EventTypeId(paramTypeIdStr);
QVariant value = PyObjectToQVariant(valueObj);
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
QMetaObject::invokeMethod(plugin, "setConfigValue", Qt::QueuedConnection, Q_ARG(ParamTypeId, paramTypeId), Q_ARG(QVariant, value));
Py_RETURN_NONE;
}
PyObject *PythonIntegrationPlugin::pyMyThings(PyObject *self, PyObject */*args*/)
{
PythonIntegrationPlugin *plugin = s_plugins.key(self);
if (!plugin) {
qCWarning(dcThingManager()) << "Cannot find plugin instance for this python module.";
return nullptr;
}
plugin->m_mutex.lock();
PyObject* result = PyTuple_New(plugin->m_things.count());
for (int i = 0; i < plugin->m_things.count(); i++) {
Thing *thing = plugin->m_things.keys().at(i);
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 &paramTypeId, const QVariant &value){
// Sync changed value to the thread-safe copy
m_mutex.lock();
m_pluginConfigCopy.setParamValue(paramTypeId, value);
m_mutex.unlock();
// And call the handler - if any
PyObject *pyParamTypeId = PyUnicode_FromString(paramTypeId.toString().toUtf8());
PyObject *pyValue = QVariantToPyObject(value);
callPluginFunction("configValueChanged", pyParamTypeId, pyValue);
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 &paramTypes, const QString &thingClassName, const QString &typeClass, const QString &typeName)
{
foreach (const ParamType &paramType, paramTypes) {
QString variableName = QString("%1ParamTypeId").arg(thingClassName + typeName[0].toUpper() + typeName.right(typeName.length()-1) + typeClass + paramType.name()[0].toUpper() + paramType.name().right(paramType.name().length() -1 ));
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;
}

View File

@ -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 &paramTypes, 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &paramTypeId) const;
Thing::ThingError setConfigValue(const ParamTypeId &paramTypeId, const QVariant &value);
Q_INVOKABLE ParamTypes configurationDescription() const;
Q_INVOKABLE Thing::ThingError setConfiguration(const ParamList &configuration);
Q_INVOKABLE ParamList configuration() const;
Q_INVOKABLE QVariant configValue(const ParamTypeId &paramTypeId) const;
Q_INVOKABLE Thing::ThingError setConfigValue(const ParamTypeId &paramTypeId, const QVariant &value);
bool isBuiltIn() const;
@ -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")

View File

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

View File

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

View File

@ -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 &paramTypeId) const;
@ -117,12 +117,12 @@ public:
QVariant paramValue(const ParamTypeId &paramTypeId) const;
void setParamValue(const ParamTypeId &paramName, const QVariant &value);
ParamList settings() const;
bool hasSetting(const ParamTypeId &paramTypeId) const;
void setSettings(const ParamList &settings);
Q_INVOKABLE ParamList settings() const;
Q_INVOKABLE bool hasSetting(const ParamTypeId &paramTypeId) const;
Q_INVOKABLE void setSettings(const ParamList &settings);
QVariant setting(const ParamTypeId &paramTypeId) const;
void setSettingValue(const ParamTypeId &paramTypeId, const QVariant &value);
Q_INVOKABLE QVariant setting(const ParamTypeId &paramTypeId) const;
Q_INVOKABLE void setSettingValue(const ParamTypeId &paramTypeId, const QVariant &value);
States states() const;
bool hasState(const StateTypeId &stateTypeId) const;

View File

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

View File

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

View File

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

View File

@ -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 &paramType, *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 &paramType, *this) {
if (paramType.id() == id) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,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"
}
]
}
]
}
]
}
]
}

View File

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

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

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

2
python-todos.txt Normal file
View File

@ -0,0 +1,2 @@
* pluginStorage missing

View File

@ -13,6 +13,7 @@ SUBDIRS = \
loggingloading \
mqttbroker \
plugins \
pythonplugins \
rules \
scripts \
states \

View File

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

View File

@ -0,0 +1,6 @@
TARGET = testpythonplugins
include(../../../nymea.pri)
include(../autotests.pri)
SOURCES += testpythonplugins.cpp

View File

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

View File

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