added Tempo plugin

This commit is contained in:
Boernsman 2021-02-17 20:59:45 +01:00
parent 68ca8605f3
commit 8150258814
12 changed files with 753 additions and 0 deletions

9
debian/control vendored
View File

@ -700,6 +700,15 @@ Depends: ${shlibs:Depends},
nymea-plugins-translations,
Description: nymea.io plugin for Telegram
This plugin allows nymea to send messages to telegram via the bot API.
Package: nymea-plugin-tempo
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Description: nymea.io plugin for Tempo time tracking
This package will install the nymea.io plugin for Tempo time tracking.
Package: nymea-plugin-tplink

1
debian/nymea-plugin-tempo.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginstempo.so

View File

@ -58,6 +58,7 @@ PLUGIN_DIRS = \
tasmota \
tcpcommander \
telegram \
tempo \
texasinstruments \
tplink \
tuya \

17
tempo/README.md Normal file
View File

@ -0,0 +1,17 @@
# Tempo
Add Tempo time tracking accounts to nymea.
## Supported Things
* Tempo Atlassian Account
* Time Accounts
## Requirements
* Package 'nymea-plugin-tempo' must be installed
* A atlassian account and client credentials
## More
https://apidocs.tempo.io/

View File

@ -0,0 +1,181 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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 "integrationplugintempo.h"
#include "plugininfo.h"
#include "network/networkaccessmanager.h"
#include <QTimer>
#include <QUrlQuery>
#include <QUrl>
#include <QNetworkRequest>
#include <QNetworkReply>
IntegrationPluginTempo::IntegrationPluginTempo()
{
}
void IntegrationPluginTempo::startPairing(ThingPairingInfo *info)
{
qCDebug(dcTempo()) << "Start pairing";
if (info->thingClassId() == tempoConnectionThingClassId) {
QByteArray clientId = configValue(tempoPluginCustomClientIdParamTypeId).toByteArray();
QByteArray clientSecret = configValue(tempoPluginCustomClientSecretParamTypeId).toByteArray();
if (clientId.isEmpty() || clientSecret.isEmpty()) {
clientId = apiKeyStorage()->requestKey("tempo").data("clientId");
clientSecret = apiKeyStorage()->requestKey("tempo").data("clientSecret");
} else {
qCDebug(dcTempo()) << "Using custom client secret and id";
}
if (clientId.isEmpty() || clientSecret.isEmpty()) {
info->finish(Thing::ThingErrorAuthenticationFailure, tr("Client id and/or seceret is not available."));
return;
}
QString jiraCloudInstanceName = info->params().paramValue(tempoConnectionAtlassianAccountParamTypeId).toString();
QUrl url = Tempo::getLoginUrl(QUrl("https://127.0.0.1:8888"), jiraCloudInstanceName, clientId);
qCDebug(dcTempo()) << "Checking if the Tempo server is reachable";
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info, url, this] {
if (reply->error() != QNetworkReply::NetworkError::HostNotFoundError) {
qCDebug(dcTempo()) << "Tempo server is reachable";
info->setOAuthUrl(url);
info->finish(Thing::ThingErrorNoError);
} else {
qCWarning(dcTempo()) << "Got online check error" << reply->error() << reply->errorString();
info->finish(Thing::ThingErrorSetupFailed, tr("Tempo server not reachable, please check the internet connection"));
}
});
} else {
qCWarning(dcTempo()) << "Unhandled pairing method!";
info->finish(Thing::ThingErrorCreationMethodNotSupported);
}
}
void IntegrationPluginTempo::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
{
Q_UNUSED(username);
qCDebug(dcTempo()) << "Confirm pairing";
if (info->thingClassId() == tempoConnectionThingClassId) {
QUrl url(secret);
QUrlQuery query(url);
QByteArray authorizationCode = query.queryItemValue("code").toLocal8Bit();
if (authorizationCode.isEmpty()) {
qCWarning(dcTempo()) << "No authorization code received.";
return info->finish(Thing::ThingErrorAuthenticationFailure);
}
Tempo *tempo = m_setupTempoConnections.value(info->thingId());
if (!tempo) {
qWarning(dcTempo()) << "No Tempo connection found for device:" << info->thingName();
m_setupTempoConnections.remove(info->thingId());
return info->finish(Thing::ThingErrorHardwareFailure);
}
qCDebug(dcTempo()) << "Authorization code" << authorizationCode.mid(0, 4)+QString().fill('*', authorizationCode.length()-4) ;
tempo->getAccessTokenFromAuthorizationCode(authorizationCode);
connect(tempo, &Tempo::receivedRefreshToken, info, [info, this](const QByteArray &refreshToken){
qCDebug(dcTempo()) << "Token:" << refreshToken.mid(0, 4)+QString().fill('*', refreshToken.length()-4) ;
pluginStorage()->beginGroup(info->thingId().toString());
pluginStorage()->setValue("refresh_token", refreshToken);
pluginStorage()->endGroup();
info->finish(Thing::ThingErrorNoError);
});
} else {
Q_ASSERT_X(false, "confirmPairing", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginTempo::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
qCDebug(dcTempo()) << "Setup thing";
if (thing->thingClassId() == tempoConnectionThingClassId) {
}
}
void IntegrationPluginTempo::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
if (thing->thingClassId() == tempoConnectionThingClassId) {
} else if (thing->thingClassId() == accountThingClassId) {
}
}
void IntegrationPluginTempo::thingRemoved(Thing *thing)
{
qCDebug(dcTempo()) << "Thing removed" << thing->name();
}
void IntegrationPluginTempo::onReceivedAccounts(const QList<Tempo::Account> &accounts)
{
qCDebug(dcTempo()) << "Received" << accounts.count() << "accounts";
Tempo *tempoConnection = static_cast<Tempo *>(sender());
Thing *parentThing = m_tempoConnections.key(tempoConnection);
if (!parentThing)
return;
ThingDescriptors desciptors;
Q_FOREACH(Tempo::Account account, accounts) {
ThingClassId thingClassId;
Thing * existingThing = myThings().findByParams(ParamList() << Param(m_idParamTypeIds.value(thingClassId), appliance.homeApplianceId));
if (existingThing) {
qCDebug(dcTempo()) << "Thing is already added to system" << existingThing->name();
//Set connected state;
//existingThing->setStateValue(m_connectedStateTypeIds.value(thingClassId), appliance.connected);
continue;
}
qCDebug(dcTempo()) << "Found new account:" << account.name << "key:" << account.key << "id:" << account.id;
ThingDescriptor descriptor(thingClassId, account.name, account.key, parentThing->id());
ParamList params;
//params << Param(m_idParamTypeIds.value(thingClassId), appliance.homeApplianceId);
descriptor.setParams(params);
desciptors.append(descriptor);
}
if (!desciptors.isEmpty())
emit autoThingsAppeared(desciptors);
}

View File

@ -0,0 +1,68 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef INTEGRATIONPLUGINTEMPO_H
#define INTEGRATIONPLUGINTEMPO_H
#include "integrations/integrationplugin.h"
#include "plugintimer.h"
#include "tempo.h"
class IntegrationPluginTempo : public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugintempo.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginTempo();
void startPairing(ThingPairingInfo *info) override;
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
void setupThing(ThingSetupInfo *info) override;
void executeAction(ThingActionInfo *info) override;
void thingRemoved(Thing *thing) override;
private:
PluginTimer *m_pluginTimer15min = nullptr;
QHash<ThingId, Tempo *> m_setupTempoConnections;
QHash<Thing *, Tempo *> m_tempoConnections;
private slots:
void onConnectionChanged(bool connected);
void onAuthenticationStatusChanged(bool authenticated);
void onRequestExecuted(QUuid requestId, bool success);
void onReceivedAccounts(const QList<Tempo::Account> &accounts);
};
#endif // INTEGRATIONPLUGINTEMPO_H

View File

@ -0,0 +1,172 @@
{
"id": "809bc4ca-d1cd-4279-9e0d-7324537ccb5a",
"name": "tempo",
"displayName": "Tempo",
"apiKeys": ["tempo"],
"paramTypes": [
{
"id": "c130b2b7-6d30-406e-899b-669a065daee3",
"name": "customClientId",
"displayName": "Custom client id",
"defaultValue": "",
"type": "QString"
},
{
"id": "9c759711-e772-44ce-9d86-6a3af89c2d94",
"name": "customClientSecret",
"displayName": "Custom client secret",
"defaultValue": "",
"type": "QString"
}
],
"vendors": [
{
"id": "58fc1ab7-b8b5-4e52-8388-72957ce5852d",
"name": "tempo",
"displayName": "Tempo",
"thingClasses": [
{
"id": "878eae0a-6217-4b36-bd46-72c911e52e73",
"name": "tempoConnection",
"displayName": "Tempo connection",
"interfaces": ["account"],
"createMethods": ["user"],
"setupMethod": "oauth",
"paramTypes": [
{
"id": "b4110c37-8331-4057-8e9f-12f34c2623fe",
"name": "atlassianAccountName",
"displayName": "Atlassian account name",
"type": "QString",
"defaultValue": ""
}
],
"stateTypes": [
{
"id": "15f45315-5419-4e1b-ace3-fc21503d3b70",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"defaultValue": true,
"cached": false,
"type": "bool"
},
{
"id": "e4b5be87-dbc9-481e-88da-608c71be8bda",
"name": "loggedIn",
"displayName": "Logged in",
"displayNameEvent": "Logged in changed",
"defaultValue": true,
"type": "bool"
},
{
"id": "f3b9581b-7828-4fbe-be5f-3e8aad78a71e",
"name": "userDisplayName",
"displayName": "User name",
"displayNameEvent": "User name changed",
"defaultValue": "",
"type": "QString"
},
{
"id": "db70444d-bf67-4133-b2de-54aefbdd7149",
"name": "autoAddAccounts",
"displayName": "Auto add accounts",
"displayNameEvent": "Auto add accounts",
"defaultValue": true,
"type": "bool"
}
]
},
{
"id": "8be71352-bdfd-450b-903e-79a4ed203701",
"name": "account",
"displayName": "Account",
"interfaces": ["connectable"],
"createMethods": ["auto"],
"browsable": true,
"paramTypes": [
{
"id": "c6aeddae-56af-496d-a419-1635ff9bae50",
"name": "id",
"displayName": "ID",
"defaultValue": "-",
"type": "QString"
}
],
"stateTypes": [
{
"id": "0b776bc1-9e56-4205-9bc3-b356026f5b64",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"defaultValue": true,
"cached": false,
"type": "bool"
},
{
"id": "7948f15b-7243-404e-9e67-18e915e8b328",
"name": "status",
"displayName": "Status",
"displayNameEvent": "Status changed",
"defaultValue": "OPEN",
"possibleValues": [
"OPEN",
"CLOSED",
"ARCHIVED"
],
"type": "QString"
},
{
"id": "abd55ea0-ad4e-413e-bc77-3e8b7f0a9be4",
"name": "global",
"displayName": "Global",
"displayNameEvent": "Global changed",
"defaultValue": false,
"type": "bool"
},
{
"id": "44ebbc18-7511-48c0-860b-c4de5f634ed6",
"name": "monthlyBudget",
"displayName": "Monthly budget",
"displayNameEvent": "Monthly budget changed",
"defaultValue": 0,
"type": "int"
},
{
"id": "f1f2af66-d09a-4242-9058-401145f662c4",
"name": "lead",
"displayName": "Lead",
"displayNameEvent": "Lead changed",
"defaultValue": "",
"type": "QString"
},
{
"id": "ece43b12-4a0d-4e25-b811-b1aca610bea8",
"name": "contact",
"displayName": "Contact",
"displayNameEvent": "Contact changed",
"defaultValue": "",
"type": "QString"
},
{
"id": "3af6d1c0-bb0a-406f-809b-2c367e1a16bb",
"name": "category",
"displayName": "Category",
"displayNameEvent": "Category changed",
"defaultValue": "",
"type": "QString"
},
{
"id": "3dcc1426-51f8-46fa-9967-5a93d7bb2633",
"name": "Customer",
"displayName": "Customer",
"displayNameEvent": "Customer changed",
"defaultValue": "",
"type": "QString"
}
]
}
]
}
]
}

0
tempo/meta.json Normal file
View File

155
tempo/tempo.cpp Normal file
View File

@ -0,0 +1,155 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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 <QJsonDocument>
#include <QUrlQuery>
#include "tempo.h"
#include "extern-plugininfo.h"
Tempo::Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent) :
QObject(parent),
m_clientId(clientId),
m_clientSecret(clientSecret),
m_networkManager(networkmanager)
{
m_tokenRefreshTimer = new QTimer(this);
m_tokenRefreshTimer->setSingleShot(true);
connect(m_tokenRefreshTimer, &QTimer::timeout, this, &Tempo::onRefreshTimer);
}
QByteArray Tempo::accessToken()
{
return m_accessToken;
}
QByteArray Tempo::refreshToken()
{
return m_refreshToken;
}
QUrl Tempo::getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName, const QByteArray &clientId)
{
QUrl url;
url.setScheme("https");
url.setHost(jiraCloudInstanceName+"atlassian.net");
url.setPath("/plugins/servlet/ac/io.tempo.jira/oauth-authorize/");
QUrlQuery query;
query.addQueryItem("client_id", clientId);
query.addQueryItem("redirect_uri", redirectUrl.toString());
query.addQueryItem("access_type", "tenant_user");
url.setQuery(query);
return url;
}
void Tempo::getAccounts()
{
QUrl url = QUrl(m_baseControlUrl+"/accounts");
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [this, reply]{
QByteArray rawData = reply->readAll();
if (!checkStatusCode(reply, rawData)) {
return;
}
QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap();
QVariantList accountList = dataMap.value("results").toList();
QList<Account> accounts;
Q_FOREACH(QVariant var, accountList) {
QVariantMap map = var.toMap();
Account account;
account.self = map["self"].toString();
account.key = map["key"].toString();
account.id = map["id"].toInt();
account.name = map["name"].toString();
if (map["status"] == "OPEN") {
account.status = Status::Open;
} else if (map["status"] == "CLOSED") {
account.status = Status::Closed;
} else if (map["status"] == "ARCHIVED") {
account.status = Status::Archived;
}
account.global = map["global"].toBool();
account.monthlyBudget = map["monthlyBudget"].toInt();
QVariantMap lead = map["lead"].toMap();
account.lead.self = lead["self"].toString();
account.lead.accountId = lead["accountId"].toString();
account.lead.displayName = lead["displayName"].toString();
//TODO Customer
QVariantMap customer = map["customer"].toMap();
account.customer.self = lead["self"].toString();
//TODO Category
QVariantMap category = map["category"].toMap();
account.category.self = lead["self"].toString();
//TODO Contact
QVariantMap contact = map["contact"].toMap();
account.contact.self = lead["self"].toString();
accounts.append(account);
}
if (!accounts.isEmpty()) {
}
});
}
void Tempo::getWorkloadByAccount(const QString &accountKey, QDate from, QDate to)
{
}
void Tempo::onRefreshTimer()
{
}
void Tempo::setAuthenticated(bool state)
{
if (state != m_authenticated) {
m_authenticated = state;
emit authenticationStatusChanged(state);
}
}
void Tempo::setConnected(bool state)
{
if (state != m_connected) {
m_connected = state;
emit connectionChanged(state);
}
}

135
tempo/tempo.h Normal file
View File

@ -0,0 +1,135 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef TEMPO_H
#define TEMPO_H
#include <QObject>
#include <QUrl>
#include <QTimer>
#include "network/networkaccessmanager.h"
class Tempo : public QObject
{
Q_OBJECT
public:
enum Status {
Open,
Closed,
Archived
};
Q_ENUM(Status)
struct Lead {
QUrl self;
QString accountId;
QString displayName;
};
struct Contact {
QUrl self;
QString accountId;
QString displayName;
QString type;
};
struct Category {
QUrl self;
QString accountId;
QString displayName;
};
struct Customer {
QUrl self;
QString key;
int id;
QString name;
};
struct Account {
QUrl self;
QString key;
int id;
QString name;
Status status;
bool global;
int monthlyBudget;
Lead lead;
Contact contact;
Category category;
Customer customer;
};
explicit Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent = nullptr);
QByteArray accessToken();
QByteArray refreshToken();
static QUrl getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName, const QByteArray &clientId);
void getAccessTokenFromRefreshToken(const QByteArray &refreshToken);
void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode);
void getAccounts();
void getWorkloadByAccount(const QString &accountKey, QDate from, QDate to);
private:
QByteArray m_baseTokenUrl = "https://api.tempo.io/oauth/token/";
QByteArray m_baseControlUrl = "https://api.tempo.io/core/3/";
QByteArray m_clientId;
QByteArray m_clientSecret;
QByteArray m_accessToken;
QByteArray m_refreshToken;
QByteArray m_redirectUri = "https://127.0.0.1:8888";
NetworkAccessManager *m_networkManager = nullptr;
QTimer *m_tokenRefreshTimer = nullptr;
void setAuthenticated(bool state);
void setConnected(bool state);
bool m_authenticated = false;
bool m_connected = false;
bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData);
private slots:
void onRefreshTimer();
signals:
void authenticationStatusChanged(bool state);
void connectionChanged(bool connected);
void receivedRefreshToken(const QByteArray &refreshToken);
void receivedAccessToken(const QByteArray &accessToken);
void accountsReceived(const QList<Account> accounts);
};
#endif // TEMPO_H

BIN
tempo/tempo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

14
tempo/tempo.pro Normal file
View File

@ -0,0 +1,14 @@
include(../plugins.pri)
QT += network
TARGET = $$qtLibraryTarget(nymea_integrationplugintempo)
SOURCES += \
integrationplugintempo.cpp \
tempo.cpp
HEADERS += \
integrationplugintempo.h \
tempo.h