diff --git a/debian/control b/debian/control
index db1cc724..03b23488 100644
--- a/debian/control
+++ b/debian/control
@@ -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
diff --git a/debian/nymea-plugin-tempo.install.in b/debian/nymea-plugin-tempo.install.in
new file mode 100644
index 00000000..c63e05ef
--- /dev/null
+++ b/debian/nymea-plugin-tempo.install.in
@@ -0,0 +1 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginstempo.so
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index eb037d01..a92e5782 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -58,6 +58,7 @@ PLUGIN_DIRS = \
tasmota \
tcpcommander \
telegram \
+ tempo \
texasinstruments \
tplink \
tuya \
diff --git a/tempo/README.md b/tempo/README.md
new file mode 100644
index 00000000..cc12441a
--- /dev/null
+++ b/tempo/README.md
@@ -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/
diff --git a/tempo/integrationplugintempo.cpp b/tempo/integrationplugintempo.cpp
new file mode 100644
index 00000000..9cc92cb4
--- /dev/null
+++ b/tempo/integrationplugintempo.cpp
@@ -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 .
+*
+* 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
+#include
+#include
+#include
+#include
+
+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 &accounts)
+{
+ qCDebug(dcTempo()) << "Received" << accounts.count() << "accounts";
+
+ Tempo *tempoConnection = static_cast(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);
+}
+
diff --git a/tempo/integrationplugintempo.h b/tempo/integrationplugintempo.h
new file mode 100644
index 00000000..5ad3bd88
--- /dev/null
+++ b/tempo/integrationplugintempo.h
@@ -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 .
+*
+* 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 m_setupTempoConnections;
+ QHash m_tempoConnections;
+
+private slots:
+ void onConnectionChanged(bool connected);
+ void onAuthenticationStatusChanged(bool authenticated);
+ void onRequestExecuted(QUuid requestId, bool success);
+ void onReceivedAccounts(const QList &accounts);
+};
+#endif // INTEGRATIONPLUGINTEMPO_H
diff --git a/tempo/integrationplugintempo.json b/tempo/integrationplugintempo.json
new file mode 100644
index 00000000..1aaede7d
--- /dev/null
+++ b/tempo/integrationplugintempo.json
@@ -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"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tempo/meta.json b/tempo/meta.json
new file mode 100644
index 00000000..e69de29b
diff --git a/tempo/tempo.cpp b/tempo/tempo.cpp
new file mode 100644
index 00000000..fc497ce9
--- /dev/null
+++ b/tempo/tempo.cpp
@@ -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 .
+*
+* 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
+#include
+
+#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 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);
+ }
+}
diff --git a/tempo/tempo.h b/tempo/tempo.h
new file mode 100644
index 00000000..c6316c1f
--- /dev/null
+++ b/tempo/tempo.h
@@ -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 .
+*
+* 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
+#include
+#include
+
+#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 accounts);
+};
+
+#endif // TEMPO_H
diff --git a/tempo/tempo.png b/tempo/tempo.png
new file mode 100644
index 00000000..28852c46
Binary files /dev/null and b/tempo/tempo.png differ
diff --git a/tempo/tempo.pro b/tempo/tempo.pro
new file mode 100644
index 00000000..f649c4e5
--- /dev/null
+++ b/tempo/tempo.pro
@@ -0,0 +1,14 @@
+include(../plugins.pri)
+
+QT += network
+
+TARGET = $$qtLibraryTarget(nymea_integrationplugintempo)
+
+SOURCES += \
+ integrationplugintempo.cpp \
+ tempo.cpp
+
+HEADERS += \
+ integrationplugintempo.h \
+ tempo.h
+