diff --git a/libnymea-core/integrations/python/pyparam.h b/libnymea-core/integrations/python/pyparam.h index ab2b71a1..387b446f 100644 --- a/libnymea-core/integrations/python/pyparam.h +++ b/libnymea-core/integrations/python/pyparam.h @@ -7,6 +7,7 @@ #include "pyutils.h" #include "types/param.h" +#include "types/paramtype.h" #include "loggingcategories.h" @@ -99,7 +100,7 @@ static PyParam* PyParam_fromParam(const Param ¶m) static Param PyParam_ToParam(PyParam *pyParam) { - ParamTypeId paramTypeId = ParamTypeId(PyUnicode_AsUTF8(pyParam->pyParamTypeId)); + ParamTypeId paramTypeId = ParamTypeId(PyUnicode_AsUTF8AndSize(pyParam->pyParamTypeId, nullptr)); QVariant value = PyObjectToQVariant(pyParam->pyValue); return Param(paramTypeId, value); } diff --git a/libnymea-core/integrations/python/pything.h b/libnymea-core/integrations/python/pything.h index 3074dd61..647a4998 100644 --- a/libnymea-core/integrations/python/pything.h +++ b/libnymea-core/integrations/python/pything.h @@ -11,7 +11,6 @@ #include #include -#include #include #pragma GCC diagnostic push @@ -26,7 +25,7 @@ * 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 lock the self->mutex while using the pointer to it for invoking 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. @@ -37,7 +36,7 @@ typedef struct _thing { PyObject_HEAD Thing *thing = nullptr; // the actual thing in nymea (not thread-safe!) - QMutex *mutex = nullptr; // The mutex for accessing the thing pointer + 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; @@ -55,7 +54,6 @@ static PyObject* PyThing_new(PyTypeObject *type, PyObject */*args*/, PyObject */ return nullptr; } qWarning() << "*++++ PyThing" << self; - self->mutex = new QMutex(); return (PyObject*)self; } @@ -64,6 +62,9 @@ static void PyThing_setThing(PyThing *self, Thing *thing) { self->thing = thing; + // 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()); @@ -106,6 +107,24 @@ static void PyThing_setThing(PyThing *self, Thing *thing) } PyGILState_Release(s); }); + + QObject::connect(thing, &Thing::stateValueChanged, [=](const StateTypeId &stateTypeId, const QVariant &value){ + PyGILState_STATE s = PyGILState_Ensure(); + 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) { + qWarning() << "Updating state" << stateTypeId << value; + pyState = Py_BuildValue("{s:s, s:O}", + "stateTypeId", stateTypeId.toString().toUtf8().data(), + "value", QVariantToPyObject(value)); + PyList_SetItem(self->pyStates, i, pyState); + break; + } + } + PyGILState_Release(s); + }); } @@ -119,8 +138,8 @@ static void PyThing_dealloc(PyThing * self) { Py_XDECREF(self->pyStates); Py_XDECREF(self->pyNameChangedHandler); Py_XDECREF(self->pySettingChangedHandler); - delete self->mutex; - Py_TYPE(self)->tp_free(self); + delete self->thingClass; + Py_TYPE(self)->tp_free(self); } static PyObject *PyThing_getName(PyThing *self, void */*closure*/) @@ -143,7 +162,6 @@ static PyObject *PyThing_getThingClassId(PyThing *self, void */*closure*/) static int PyThing_setName(PyThing *self, PyObject *value, void */*closure*/){ QString name = QString(PyUnicode_AsUTF8(value)); - QMutexLocker(self->mutex); if (!self->thing) { return -1; } @@ -151,6 +169,74 @@ static int PyThing_setName(PyThing *self, PyObject *value, void */*closure*/){ return 0; } +static PyObject * PyThing_paramValue(PyThing* self, PyObject* args) +{ + char *paramTypeIdStr = nullptr; + + if (!PyArg_ParseTuple(args, "s", ¶mTypeIdStr)) { + qCWarning(dcThingManager) << "Error parsing parameters"; + return nullptr; + } + + ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr); + PyObject *iterator = PyObject_GetIter(self->pyParams); + while (iterator) { + PyObject *pyParam = PyIter_Next(iterator); + if (!pyParam) { + break; + } + + Param param = PyParam_ToParam((PyParam*)pyParam); + Py_DECREF(pyParam); + + if (param.paramTypeId() != paramTypeId) { + continue; + } + + Py_DECREF(iterator); + + return QVariantToPyObject(param.value()); + } + + Py_DECREF(iterator); + qCWarning(dcPythonIntegrations()) << "No param for paramTypeId:" << paramTypeId; + Py_RETURN_NONE; +} + +static PyObject * PyThing_setting(PyThing* self, PyObject* args) +{ + char *paramTypeIdStr = nullptr; + + if (!PyArg_ParseTuple(args, "s", ¶mTypeIdStr)) { + qCWarning(dcThingManager) << "Error parsing parameters"; + return nullptr; + } + + ParamTypeId paramTypeId = ParamTypeId(paramTypeIdStr); + PyObject *iterator = PyObject_GetIter(self->pySettings); + while (iterator) { + PyObject *pyParam = PyIter_Next(iterator); + if (!pyParam) { + break; + } + + Param param = PyParam_ToParam((PyParam*)pyParam); + Py_DECREF(pyParam); + + if (param.paramTypeId() != paramTypeId) { + continue; + } + + Py_DECREF(iterator); + + return QVariantToPyObject(param.value()); + } + + Py_DECREF(iterator); + qCWarning(dcPythonIntegrations()) << "No setting for paramTypeId:" << paramTypeId; + Py_RETURN_NONE; +} + static PyObject *PyThing_getSettings(PyThing *self, void */*closure*/) { Py_INCREF(self->pySettings); @@ -167,42 +253,25 @@ static PyObject * PyThing_stateValue(PyThing* self, PyObject* args) char *stateTypeIdStr = nullptr; if (!PyArg_ParseTuple(args, "s", &stateTypeIdStr)) { - qCWarning(dcThingManager) << "Error parsing parameters"; + PyErr_SetString(PyExc_ValueError, "Error parsing arguments. Signature is 's'"); return nullptr; } StateTypeId stateTypeId = StateTypeId(stateTypeIdStr); - PyObject *iterator = PyObject_GetIter(self->pyStates); - while (iterator) { - PyObject *pyState = PyIter_Next(iterator); - if (!pyState) { - break; - } + for (int i = 0; i < PyList_Size(self->pyStates); i++) { + PyObject *pyState = PyList_GetItem(self->pyStates, i); PyObject *pyStateTypeId = PyDict_GetItemString(pyState, "stateTypeId"); - PyObject *tmp = PyUnicode_AsEncodedString(pyStateTypeId, "UTF-8", "strict"); - - StateTypeId stid = StateTypeId(PyBytes_AS_STRING(tmp)); - - Py_DECREF(pyStateTypeId); - Py_XDECREF(tmp); - - if (stid != stateTypeId) { - Py_DECREF(pyState); - continue; + StateTypeId stid = StateTypeId(PyUnicode_AsUTF8AndSize(pyStateTypeId, nullptr)); + if (stid == stateTypeId) { + PyObject *value = PyDict_GetItemString(pyState, "value"); + Py_INCREF(value); + return value; } - - PyObject *pyStateValue = PyDict_GetItemString(pyState, "value"); - - Py_DECREF(pyState); - Py_DECREF(iterator); - - return pyStateValue; } - Py_DECREF(iterator); - qCWarning(dcPythonIntegrations()) << "No state for stateTypeId:" << stateTypeId; - Py_RETURN_NONE; + 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) @@ -211,14 +280,18 @@ static PyObject * PyThing_setStateValue(PyThing* self, PyObject* args) PyObject *valueObj = nullptr; if (!PyArg_ParseTuple(args, "sO", &stateTypeIdStr, &valueObj)) { - qCWarning(dcThingManager) << "Error parsing parameters"; + PyErr_SetString(PyExc_ValueError, "Error parsing arguments. Signature is 'sO'"); return nullptr; } StateTypeId stateTypeId = StateTypeId(stateTypeIdStr); + StateType stateType = self->thingClass->stateTypes().findById(stateTypeId); + if (!stateType.isValid()) { + PyErr_SetString(PyExc_ValueError, QString("No state type %1 in thing class %2").arg(stateTypeId.toString()).arg(self->thingClass->name()).toUtf8()); + return nullptr; + } QVariant value = PyObjectToQVariant(valueObj); - QMutexLocker(self->mutex); if (self->thing != nullptr) { QMetaObject::invokeMethod(self->thing, "setStateValue", Qt::QueuedConnection, Q_ARG(StateTypeId, stateTypeId), Q_ARG(QVariant, value)); } @@ -232,14 +305,23 @@ static PyObject * PyThing_emitEvent(PyThing* self, PyObject* args) PyObject *valueObj = nullptr; if (!PyArg_ParseTuple(args, "s|O", &eventTypeIdStr, &valueObj)) { - qCWarning(dcThingManager) << "Error parsing parameters"; + 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); - QMutexLocker(self->mutex); if (self->thing != nullptr) { QMetaObject::invokeMethod(self->thing, "emitEvent", Qt::QueuedConnection, Q_ARG(EventTypeId, eventTypeId), Q_ARG(ParamList, params)); } @@ -256,9 +338,11 @@ static PyGetSetDef PyThing_getset[] = { }; static PyMethodDef PyThing_methods[] = { - { "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" }, + { "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 }; diff --git a/libnymea-core/integrations/python/pyutils.h b/libnymea-core/integrations/python/pyutils.h index a119b5e6..0e0ef81a 100644 --- a/libnymea-core/integrations/python/pyutils.h +++ b/libnymea-core/integrations/python/pyutils.h @@ -45,18 +45,21 @@ PyObject *QVariantToPyObject(const QVariant &value) QVariant PyObjectToQVariant(PyObject *pyObject) { - // FIXME: is there any better way to do this? - qWarning() << "Error:" << PyErr_CheckSignals(); - PyObject* repr = PyObject_Repr(pyObject); - PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); - const char *bytes = PyBytes_AS_STRING(str); + qWarning() << "**************** type" << pyObject->ob_type->tp_name; - QVariant value(bytes); + if (qstrcmp(pyObject->ob_type->tp_name, "int") == 0) { + return QVariant(PyLong_AsLongLong(pyObject)); + } - Py_XDECREF(repr); - Py_XDECREF(str); + if (qstrcmp(pyObject->ob_type->tp_name, "str") == 0) { + return QVariant(PyUnicode_AsUTF8AndSize(pyObject, nullptr)); + } - return value; + if (qstrcmp(pyObject->ob_type->tp_name, "double") == 0) { + return QVariant(PyFloat_AsDouble(pyObject)); + } + Q_ASSERT_X(false, "pyutils.h", "Unhandled data type in conversion from Param to PyParam!"); + return QVariant(); } diff --git a/libnymea/types/paramtype.cpp b/libnymea/types/paramtype.cpp index 13a6ca85..fd0758c4 100644 --- a/libnymea/types/paramtype.cpp +++ b/libnymea/types/paramtype.cpp @@ -272,7 +272,7 @@ void ParamTypes::put(const QVariant &variant) append(variant.value()); } -ParamType ParamTypes::findByName(const QString &name) +ParamType ParamTypes::findByName(const QString &name) const { foreach (const ParamType ¶mType, *this) { if (paramType.name() == name) { @@ -282,7 +282,7 @@ ParamType ParamTypes::findByName(const QString &name) return ParamType(); } -ParamType ParamTypes::findById(const ParamTypeId &id) +ParamType ParamTypes::findById(const ParamTypeId &id) const { foreach (const ParamType ¶mType, *this) { if (paramType.id() == id) { diff --git a/libnymea/types/paramtype.h b/libnymea/types/paramtype.h index ea4df364..8f8cede2 100644 --- a/libnymea/types/paramtype.h +++ b/libnymea/types/paramtype.h @@ -124,8 +124,8 @@ public: ParamTypes(const QList &other); Q_INVOKABLE QVariant get(int index) const; Q_INVOKABLE void put(const QVariant &variant); - ParamType findByName(const QString &name); - ParamType findById(const ParamTypeId &id); + ParamType findByName(const QString &name) const; + ParamType findById(const ParamTypeId &id) const; }; Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(ParamTypes) diff --git a/libnymea/types/statetype.h b/libnymea/types/statetype.h index f87e91eb..1532a434 100644 --- a/libnymea/types/statetype.h +++ b/libnymea/types/statetype.h @@ -103,7 +103,7 @@ private: QString m_name; QString m_displayName; int m_index = 0; - QVariant::Type m_type; + QVariant::Type m_type = QVariant::Invalid; QVariant m_defaultValue; QVariant m_minValue; QVariant m_maxValue; diff --git a/plugins/pymock/integrationpluginpymock.json b/plugins/pymock/integrationpluginpymock.json index 01b89a3e..39636d8a 100644 --- a/plugins/pymock/integrationpluginpymock.json +++ b/plugins/pymock/integrationpluginpymock.json @@ -8,7 +8,7 @@ "name": "autoThingCount", "displayName": "Number of auto things", "type": "int", - "defaultValue": "fds" + "defaultValue": 0 } ], "vendors": [ @@ -94,6 +94,21 @@ "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", diff --git a/plugins/pymock/integrationpluginpymock.py b/plugins/pymock/integrationpluginpymock.py index 0aa6b620..3c4b2cc0 100644 --- a/plugins/pymock/integrationpluginpymock.py +++ b/plugins/pymock/integrationpluginpymock.py @@ -3,9 +3,19 @@ import asyncio watchingAutoThings = False -def init(): +async def init(): logger.log("Python mock plugin init") + while True: + await asyncio.sleep(2); + logger.log("Updating stuff") + for thing in myThings(): + 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) + def configValueChanged(paramTypeId, value): logger.log("Plugin config value changed:", paramTypeId, value, watchingAutoThings) @@ -74,7 +84,7 @@ async def setupThing(info): info.finish(nymea.ThingErrorNoError) -def postSetupThing(thing): +async def postSetupThing(thing): logger.log("postSetupThing for", thing.name, thing.params[0].value) thing.nameChangedHandler = lambda thing : logger.log("Thing name changed", thing.name) @@ -82,7 +92,9 @@ def postSetupThing(thing): logger.log("State 1 value:", thing.stateValue(pyMockAutoState1StateTypeId)) if thing.thingClassId == pyMockDiscoveryPairingThingClassId: - logger.log("Setting 1 value:", thing.settingsValue(pyMockDiscoveryPairingSettingsSetting1ParamTypeId)) + logger.log("Param 1 value:", thing.paramValue(pyMockDiscoveryPairingThingParam1ParamTypeId)) + logger.log("Setting 1 value:", thing.setting(pyMockDiscoveryPairingSettingsSetting1ParamTypeId)) + def autoThings():