mirror of https://github.com/nymea/nymea.git
Merge PR #451: Add PluginTimer API to python plugins
commit
e0fc43b308
|
|
@ -17,6 +17,7 @@
|
|||
#include "pybrowseritem.h"
|
||||
#include "pybrowseractioninfo.h"
|
||||
#include "pybrowseritemresult.h"
|
||||
#include "pyplugintimer.h"
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
|
||||
|
|
@ -39,6 +40,7 @@ static int nymea_exec(PyObject *m) {
|
|||
registerBrowserItemType(m);
|
||||
registerBrowserActionInfoType(m);
|
||||
registerBrowserItemResultType(m);
|
||||
registerPluginTimerType(m);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
#ifndef PYPLUGINTIMER_H
|
||||
#define PYPLUGINTIMER_H
|
||||
|
||||
#include <Python.h>
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#include <QCoreApplication>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include "structmember.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 *pyTimeoutHandler = nullptr;
|
||||
QTimer *timer;
|
||||
int interval;
|
||||
PyInterpreterState *interpreter;
|
||||
} PyPluginTimer;
|
||||
|
||||
static int PyPluginTimer_init(PyPluginTimer *self, PyObject *args, PyObject */*kwds*/)
|
||||
{
|
||||
PyObject *handler;
|
||||
|
||||
qCDebug(dcPythonIntegrations()) << "+++ PyPluginTimer";
|
||||
if (!PyArg_ParseTuple(args, "i|O", &self->interval, &handler)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// QTimer needs to be run in a thread that has a QEventLoop but we don't necessarily have one in
|
||||
// python threads. So we're moving the timer to the main app thread.
|
||||
self->timer = new QTimer();
|
||||
self->timer->start(self->interval * 1000);
|
||||
self->timer->moveToThread(QCoreApplication::instance()->thread());
|
||||
|
||||
self->pyTimeoutHandler = handler;
|
||||
Py_XINCREF(handler);
|
||||
|
||||
// Remember the interpreter from the current thread so we can run the callback in the correct interpreter
|
||||
self->interpreter = PyThreadState_GET()->interp;
|
||||
|
||||
|
||||
QObject::connect(self->timer, &QTimer::timeout, [=](){
|
||||
qCDebug(dcPythonIntegrations) << "Plugin timer timeout" << self->pyTimeoutHandler;
|
||||
|
||||
// Spawn a new thread for the callback of the timer (like we do for every python call).
|
||||
// FIXME: Ideally we'd use the plugin's thread pool but we can't easily access that here.
|
||||
// If the timer callback blocks for longer than the timer interval, we might end up with
|
||||
// tons of threads...
|
||||
QFuture<void> future = QtConcurrent::run([=](){
|
||||
// Acquire GIL and make the new thread state the current one
|
||||
PyThreadState *threadState = PyThreadState_New(self->interpreter);
|
||||
PyEval_RestoreThread(threadState);
|
||||
|
||||
if (self->pyTimeoutHandler) {
|
||||
|
||||
PyObject *ret = PyObject_CallFunction(self->pyTimeoutHandler, nullptr);
|
||||
if (PyErr_Occurred()) {
|
||||
PyErr_Print();
|
||||
}
|
||||
Py_XDECREF(ret);
|
||||
}
|
||||
PyThreadState_Clear(threadState);
|
||||
PyEval_ReleaseThread(threadState);
|
||||
PyThreadState_Delete(threadState);
|
||||
});
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void PyPluginTimer_dealloc(PyPluginTimer * self)
|
||||
{
|
||||
qCDebug(dcPythonIntegrations()) << "--- PyPluginTimer";
|
||||
Py_XDECREF(self->pyTimeoutHandler);
|
||||
|
||||
QMetaObject::invokeMethod(self->timer, "stop", Qt::QueuedConnection);
|
||||
self->timer->deleteLater();
|
||||
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
}
|
||||
|
||||
|
||||
static PyObject *PyPluginTimer_getInterval(PyPluginTimer *self, void */*closure*/)
|
||||
{
|
||||
return PyLong_FromLong(self->interval);
|
||||
}
|
||||
|
||||
static int PyPluginTimer_setInterval(PyPluginTimer *self, PyObject *value, void */*closure*/){
|
||||
self->interval = PyLong_AsLong(value);
|
||||
QMetaObject::invokeMethod(self->timer, "start", Qt::QueuedConnection, Q_ARG(int, self->interval * 1000));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PyGetSetDef PyPluginTimer_getset[] = {
|
||||
{"interval", (getter)PyPluginTimer_getInterval, (setter)PyPluginTimer_setInterval, "Timer interval", nullptr},
|
||||
{nullptr , nullptr, nullptr, nullptr, nullptr} /* Sentinel */
|
||||
};
|
||||
|
||||
|
||||
static PyMemberDef PyPluginTimer_members[] = {
|
||||
{"timeoutHandler", T_OBJECT_EX, offsetof(PyPluginTimer, pyTimeoutHandler), 0, "Set a callback for when the timer timeout triggers."},
|
||||
{nullptr, 0, 0, 0, nullptr} /* Sentinel */
|
||||
};
|
||||
|
||||
static PyTypeObject PyPluginTimerType = {
|
||||
PyVarObject_HEAD_INIT(NULL, 0)
|
||||
"nymea.PluginTimer", /* tp_name */
|
||||
sizeof(PyPluginTimer), /* tp_basicsize */
|
||||
0, /* tp_itemsize */
|
||||
(destructor)PyPluginTimer_dealloc, /* tp_dealloc */
|
||||
};
|
||||
|
||||
|
||||
|
||||
static void registerPluginTimerType(PyObject *module)
|
||||
{
|
||||
PyPluginTimerType.tp_new = PyType_GenericNew;
|
||||
PyPluginTimerType.tp_members = PyPluginTimer_members;
|
||||
PyPluginTimerType.tp_getset = PyPluginTimer_getset;
|
||||
PyPluginTimerType.tp_init = reinterpret_cast<initproc>(PyPluginTimer_init);
|
||||
PyPluginTimerType.tp_doc = "PluginTimers can be used to perform repeating tasks, such as polling a device or online service.";
|
||||
PyPluginTimerType.tp_flags = Py_TPFLAGS_DEFAULT;
|
||||
|
||||
if (PyType_Ready(&PyPluginTimerType) < 0) {
|
||||
return;
|
||||
}
|
||||
PyModule_AddObject(module, "PluginTimer", reinterpret_cast<PyObject*>(&PyPluginTimerType));
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#endif // PYPLUGINTIMER_H
|
||||
|
|
@ -50,6 +50,7 @@ public:
|
|||
static PyObject* pyAutoThingDisappeared(PyObject *self, PyObject* args);
|
||||
static PyObject* pyPluginStorage(PyObject* self, PyObject* args);
|
||||
static PyObject* pyApiKeyStorage(PyObject* self, PyObject* args);
|
||||
static PyObject* pyHardwareManager(PyObject* self, PyObject* args);
|
||||
|
||||
private:
|
||||
void exportIds();
|
||||
|
|
|
|||
|
|
@ -1370,7 +1370,7 @@ void ThingManagerImplementation::loadPlugins()
|
|||
delete p;
|
||||
}
|
||||
#else
|
||||
qCWarning(dcThingManager()) << "Not loading Python plugin as Python plugin support is not included in this nymea instance.2";
|
||||
qCWarning(dcThingManager()) << "Not loading Python plugin as Python plugin support is not included in this nymea instance.";
|
||||
#endif
|
||||
} else {
|
||||
// Not a known plugin type
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ HEADERS += nymeacore.h \
|
|||
integrations/python/pybrowseritem.h \
|
||||
integrations/python/pybrowseritemresult.h \
|
||||
integrations/python/pypluginstorage.h \
|
||||
integrations/python/pyplugintimer.h \
|
||||
integrations/thingmanagerimplementation.h \
|
||||
integrations/translator.h \
|
||||
experiences/experiencemanager.h \
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@
|
|||
"createMethods": ["user"],
|
||||
"setupMethod": "justAdd",
|
||||
"browsable": true,
|
||||
"settingsTypes": [
|
||||
{
|
||||
"id": "f3ef303b-d719-4a64-a44c-6b1935bf0d4d",
|
||||
"name": "interval",
|
||||
"displayName": "Timer interval",
|
||||
"type": "uint",
|
||||
"defaultValue": 5
|
||||
}
|
||||
],
|
||||
"eventTypes": [
|
||||
{
|
||||
"id": "de6c2425-0dee-413f-8f4c-bb0929e83c0d",
|
||||
|
|
|
|||
|
|
@ -1,59 +1,24 @@
|
|||
import nymea
|
||||
import time
|
||||
#from fastdotcom import fast_com
|
||||
|
||||
watchingAutoThings = False
|
||||
loopRunning = False
|
||||
globalPluginTimer = None
|
||||
pluginTimers = {}
|
||||
|
||||
# Optional, for initialisation, if needed
|
||||
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")
|
||||
|
||||
|
||||
# Optional, clean up stuff if needed
|
||||
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)
|
||||
|
||||
|
||||
# Optional, if the plugin should create auto things, this is the right place to create them,
|
||||
# or, start monitoring the network (or whatever) for them to appear
|
||||
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)):
|
||||
|
|
@ -68,13 +33,14 @@ def startMonitoringAutoThings():
|
|||
logger.log("Done start monitoring auto things")
|
||||
|
||||
|
||||
# If the plugin supports things of createMethod "discovery", nymea will call this to discover 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
|
||||
time.sleep(5) # Some delay for giving a feeling of a real discovery
|
||||
# Add discovery results (in this example the amount given by the discovery params)
|
||||
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
|
||||
# Also add existing ones again so reconfiguration is possible, setting the existing thing ID properly
|
||||
for thing in myThings():
|
||||
if thing.thingClassId == pyMockDiscoveryPairingThingClassId:
|
||||
info.addDescriptor(nymea.ThingDescriptor(pyMockDiscoveryPairingThingClassId, thing.name, thingId=thing.id))
|
||||
|
|
@ -82,11 +48,13 @@ def discoverThings(info):
|
|||
info.finish(nymea.ThingErrorNoError)
|
||||
|
||||
|
||||
# If the plugin supports things with a setupMethod other than "justAdd", this will be called to initiate login/pairing
|
||||
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\".")
|
||||
|
||||
|
||||
# If the plugin supports things with a setupMethod other than "justAdd", this will be called to complete login/pairing
|
||||
def confirmPairing(info, username, secret):
|
||||
logger.log("confirming pairing for", info.thingName, username, secret)
|
||||
time.sleep(1)
|
||||
|
|
@ -96,16 +64,23 @@ def confirmPairing(info, username, secret):
|
|||
info.finish(nymea.ThingErrorAuthenticationFailure, "Error logging in here!")
|
||||
|
||||
|
||||
# Mandatory, a new thing is being set up. Initialize (connect etc...) it
|
||||
def setupThing(info):
|
||||
logger.log("setupThing for", info.thing.name)
|
||||
info.finish(nymea.ThingErrorNoError)
|
||||
info.thing.nameChangedHandler = thingNameChanged
|
||||
|
||||
# Signal handlers
|
||||
info.thing.settingChangedHandler = thingSettingChanged
|
||||
info.thing.nameChangedHandler = lambda info : logger.log("Thing name changed", info.thing.name)
|
||||
|
||||
|
||||
# Optional, run additional code after a successful thing setup
|
||||
def postSetupThing(thing):
|
||||
logger.log("postSetupThing for", thing.name)
|
||||
thing.nameChangedHandler = lambda thing : logger.log("Thing name changed", thing.name)
|
||||
|
||||
global globalPluginTimer
|
||||
if globalPluginTimer is None:
|
||||
globalPluginTimer = nymea.PluginTimer(5, timerTriggered)
|
||||
|
||||
if thing.thingClassId == pyMockAutoThingClassId:
|
||||
logger.log("State 1 value:", thing.stateValue(pyMockAutoState1StateTypeId))
|
||||
|
|
@ -114,7 +89,46 @@ def postSetupThing(thing):
|
|||
logger.log("Param 1 value:", thing.paramValue(pyMockDiscoveryPairingThingParam1ParamTypeId))
|
||||
logger.log("Setting 1 value:", thing.setting(pyMockDiscoveryPairingSettingsSetting1ParamTypeId))
|
||||
|
||||
if thing.thingClassId == pyMockThingClassId:
|
||||
interval = thing.setting(pyMockSettingsIntervalParamTypeId)
|
||||
|
||||
pluginTimer = nymea.PluginTimer(interval, lambda thing=thing : logger.log("Timer triggered for %s. (Interval: %i)" % (thing.name, thing.setting(pyMockSettingsIntervalParamTypeId))))
|
||||
|
||||
logger.log("Thing timer interval for %s: %i" % (thing.name, pluginTimer.interval))
|
||||
pluginTimers[thing] = pluginTimer
|
||||
|
||||
|
||||
# Optional, do cleanups when a thing is removed
|
||||
def thingRemoved(thing):
|
||||
logger.log("thingRemoved for", thing.name)
|
||||
logger.log("Remaining things:", len(myThings()))
|
||||
|
||||
if thing.thingClassId == pyMockThingClassId:
|
||||
del pluginTimers[thing]
|
||||
|
||||
# Clean up the global plugin timer if there are no things left
|
||||
if len(myThings()) == 0:
|
||||
global globalPluginTimer
|
||||
globalPluginTimer = None
|
||||
|
||||
|
||||
# Callback for the plugin timer. If polling is needed, fetch values and set thing states accordingly
|
||||
def timerTriggered():
|
||||
logger.log("Timer triggered")
|
||||
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)
|
||||
|
||||
|
||||
# If the plugin supports things with actions, nymea will call this to run actions
|
||||
def executeAction(info):
|
||||
logger.log("executeAction for", info.thing.name, info.actionTypeId, "with params", info.params)
|
||||
paramValueByIndex = info.params[0].value
|
||||
|
|
@ -123,22 +137,35 @@ def executeAction(info):
|
|||
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)
|
||||
|
||||
|
||||
# Callback handler when the user changes settings for a particular thing
|
||||
def thingSettingChanged(thing, paramTypeId, value):
|
||||
logger.log("Thing setting changed:", thing.name, paramTypeId, value)
|
||||
|
||||
if thing.thingClassId == pyMockThingClassId:
|
||||
if paramTypeId == pyMockSettingsIntervalParamTypeId:
|
||||
logger.log("Adjusting plugin timer to interval:", value)
|
||||
timer = pluginTimers[thing]
|
||||
timer.interval = value
|
||||
|
||||
|
||||
# Callback handler when the user changes the global plugin configuration
|
||||
def configValueChanged(paramTypeId, value):
|
||||
logger.log("Plugin config value changed:", paramTypeId, value)
|
||||
if 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)
|
||||
|
||||
|
||||
# If a plugin supports browsable things, nymea will call this to browse a thing
|
||||
def browseThing(result):
|
||||
logger.log("browseThing called", result.thing.name, result.itemId)
|
||||
if result.itemId == "":
|
||||
|
|
@ -155,11 +182,17 @@ def browseThing(result):
|
|||
result.finish(nymea.ThingErrorNoError)
|
||||
|
||||
|
||||
def executeBrowserItem(info):
|
||||
logger.log("executeBrowserItem called for thing", info.thing.name, "and item", info.itemId)
|
||||
info.finish(nymea.ThingErrorNoError)
|
||||
|
||||
|
||||
# If a thingclass supports browser item actions, nymea will call this upon execution
|
||||
# Intentionally commented out to also have a test case for unimplmented functions
|
||||
# def thingRemoved(thing):
|
||||
# logger.log("thingRemoved for", thing.name)
|
||||
#def executeBrowserItem(info):
|
||||
# logger.log("executeBrowserItem called for thing", info.thing.name, "and item", info.itemId)
|
||||
# info.finish(nymea.ThingErrorNoError)
|
||||
|
||||
|
||||
# Helper functions can be added too
|
||||
def autoThings():
|
||||
autoThings = []
|
||||
for thing in myThings():
|
||||
if thing.thingClassId == pyMockAutoThingClassId:
|
||||
autoThings.append(thing)
|
||||
return autoThings
|
||||
|
|
|
|||
Loading…
Reference in New Issue