diff --git a/debian/control b/debian/control
index db1cc724..32d75ec9 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
@@ -1164,6 +1173,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-pushnotifications,
nymea-plugin-wakeonlan,
nymea-plugin-tasmota,
+ nymea-plugin-tempo,
nymea-plugin-tplink,
nymea-plugin-wemo,
nymea-plugin-elgato,
diff --git a/debian/nymea-plugin-tempo.install.in b/debian/nymea-plugin-tempo.install.in
new file mode 100644
index 00000000..00a44b17
--- /dev/null
+++ b/debian/nymea-plugin-tempo.install.in
@@ -0,0 +1 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugintempo.so
diff --git a/homeconnect/homeconnect.h b/homeconnect/homeconnect.h
index ae2f757f..486bb651 100644
--- a/homeconnect/homeconnect.h
+++ b/homeconnect/homeconnect.h
@@ -166,6 +166,7 @@ private:
bool m_connected = false;
bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData);
+
private slots:
void onRefreshTimeout();
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..06378a46
--- /dev/null
+++ b/tempo/integrationplugintempo.cpp
@@ -0,0 +1,466 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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) {
+
+ qCDebug(dcTempo()) << "Checking if the Tempo server is reachable: https://api.tempo.io/core/3";
+ QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(QUrl("https://api.tempo.io/core/3")));
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, info, [reply, info] {
+
+ if (reply->error() != QNetworkReply::NetworkError::HostNotFoundError) {
+ qCDebug(dcTempo()) << "Tempo server is reachable";
+ info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter your Tempo API integration token."));
+ } 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);
+
+ if (info->thingClassId() == tempoConnectionThingClassId) {
+ qCDebug(dcTempo()) << "Confirm pairing" << info->thingName();
+ if (secret.isEmpty()) {
+ qCWarning(dcTempo()) << "No authorization code received.";
+ return info->finish(Thing::ThingErrorAuthenticationFailure);
+ }
+ Tempo *tempo = new Tempo(hardwareManager()->networkManager(), secret, this);
+ tempo->getAccounts();
+ connect(info, &ThingPairingInfo::aborted, tempo, &Tempo::deleteLater);
+ connect(tempo, &Tempo::authenticationStatusChanged, info, [info, tempo, secret, this] (bool authenticated){
+ if (authenticated) {
+ pluginStorage()->beginGroup(info->thingId().toString());
+ pluginStorage()->setValue("token", secret);
+ pluginStorage()->endGroup();
+ m_setupTempoConnections.insert(info->thingId(), tempo);
+ info->finish(Thing::ThingErrorNoError);
+ }
+ });
+ } else {
+ Q_ASSERT_X(false, "confirmPairing", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
+ }
+}
+
+void IntegrationPluginTempo::discoverThings(ThingDiscoveryInfo *info)
+{
+ qCDebug(dcTempo()) << "Discover things";
+ if (m_tempoConnections.isEmpty()) {
+ return info->finish(Thing::ThingErrorHardwareNotAvailable, tr("Create a Tempo connection first"));
+ }
+
+ if (info->thingClassId() == accountThingClassId) {
+ Q_FOREACH(Tempo *tempo, m_tempoConnections) {
+ tempo->getAccounts();
+ ThingId parentThingId = m_tempoConnections.key(tempo);
+ if (parentThingId.isNull()) {
+ qCWarning(dcTempo()) << "Parent not found";
+ return;
+ }
+ connect(tempo, &Tempo::accountsReceived, info, [info, parentThingId] (const QList &accounts) {
+ Q_FOREACH(Tempo::Account account, accounts) {
+ if (account.status == Tempo::Status::Archived)
+ continue;
+
+ ThingDescriptor descriptor(accountThingClassId, account.name, account.customer.name, parentThingId);
+ ParamList params;
+ params << Param(accountThingKeyParamTypeId, account.key);
+ descriptor.setParams(params);
+ info->addThingDescriptor(descriptor);
+ }
+ });
+ }
+ } else if (info->thingClassId() == teamThingClassId) {
+ Q_FOREACH(Tempo *tempo, m_tempoConnections) {
+ tempo->getTeams();
+ ThingId parentThingId = m_tempoConnections.key(tempo);
+ if (parentThingId.isNull()) {
+ qCWarning(dcTempo()) << "Parent not found";
+ return;
+ }
+ connect(tempo, &Tempo::teamsReceived, info, [info, parentThingId] (const QList &teams) {
+ Q_FOREACH(Tempo::Team team, teams) {
+
+ ThingDescriptor descriptor(teamThingClassId, team.name, team.summary, parentThingId);
+ ParamList params;
+ params << Param(teamThingIdParamTypeId, team.id);
+ descriptor.setParams(params);
+ info->addThingDescriptor(descriptor);
+ }
+ });
+ }
+ }
+ QTimer::singleShot(5000, info, [info] {
+ info->finish(Thing::ThingErrorNoError);
+ });
+}
+
+void IntegrationPluginTempo::setupThing(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+ qCDebug(dcTempo()) << "Setup thing" << thing->name();
+
+ if (thing->thingClassId() == tempoConnectionThingClassId) {
+
+ Tempo *tempo;
+ if (m_tempoConnections.contains(thing->id())) {
+ qCDebug(dcTempo()) << "Setup after reconfiguration, cleaning up";
+ m_tempoConnections.take(thing->id())->deleteLater();
+ }
+ if (m_setupTempoConnections.keys().contains(thing->id())) {
+ // This thing setup is after a pairing process
+ qCDebug(dcTempo()) << "Tempo OAuth setup complete";
+ tempo = m_setupTempoConnections.take(thing->id());
+ if (!tempo) {
+ qCWarning(dcTempo()) << "Tempo connection object not found for thing" << thing->name();
+ }
+ m_tempoConnections.insert(thing->id(), tempo);
+ connect(tempo, &Tempo::connectionChanged, this, &IntegrationPluginTempo::onConnectionChanged);
+ connect(tempo, &Tempo::authenticationStatusChanged, this, &IntegrationPluginTempo::onAuthenticationStatusChanged);
+ connect(tempo, &Tempo::accountsReceived, this, &IntegrationPluginTempo::onAccountsReceived);
+ connect(tempo, &Tempo::teamsReceived, this, &IntegrationPluginTempo::onTeamsReceived);
+ connect(tempo, &Tempo::accountWorklogsReceived, this, &IntegrationPluginTempo::onAccountWorkloadReceived);
+ connect(tempo, &Tempo::teamWorklogsReceived, this, &IntegrationPluginTempo::onTeamWorkloadReceived);
+ info->finish(Thing::ThingErrorNoError);
+ } else {
+ //device loaded from the device database, needs a new access token;
+ pluginStorage()->beginGroup(thing->id().toString());
+ QByteArray token = pluginStorage()->value("token").toByteArray();
+ pluginStorage()->endGroup();
+ if (token.isEmpty()) {
+ info->finish(Thing::ThingErrorAuthenticationFailure, tr("Token is not available."));
+ return;
+ }
+ Tempo *tempo = new Tempo(hardwareManager()->networkManager(), token, this);
+ connect(info, &ThingSetupInfo::aborted, tempo, &Tempo::deleteLater);
+ connect(tempo, &Tempo::authenticationStatusChanged, info, [info, tempo, this] (bool authenticated){
+ if (authenticated) {
+ m_tempoConnections.insert(info->thing()->id(), tempo);
+ connect(tempo, &Tempo::connectionChanged, this, &IntegrationPluginTempo::onConnectionChanged);
+ connect(tempo, &Tempo::authenticationStatusChanged, this, &IntegrationPluginTempo::onAuthenticationStatusChanged);
+ connect(tempo, &Tempo::accountsReceived, this, &IntegrationPluginTempo::onAccountsReceived);
+ connect(tempo, &Tempo::teamsReceived, this, &IntegrationPluginTempo::onTeamsReceived);
+ connect(tempo, &Tempo::accountWorklogsReceived, this, &IntegrationPluginTempo::onAccountWorkloadReceived);
+ connect(tempo, &Tempo::teamWorklogsReceived, this, &IntegrationPluginTempo::onTeamWorkloadReceived);
+ info->finish(Thing::ThingErrorNoError);
+ }
+ });
+ tempo->getAccounts();
+ }
+
+ } else if (thing->thingClassId() == accountThingClassId ||
+ thing->thingClassId() == teamThingClassId){
+ Thing *parentThing = myThings().findById(thing->parentId());
+ if (parentThing->setupComplete()) {
+ info->finish(Thing::ThingErrorNoError);
+ } else {
+ connect(parentThing, &Thing::setupStatusChanged, info, [parentThing, info]{
+ if (parentThing->setupComplete()) {
+ info->finish(Thing::ThingErrorNoError);
+ }
+ });
+ }
+ } else {
+ Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
+ }
+}
+
+void IntegrationPluginTempo::postSetupThing(Thing *thing)
+{
+ qCDebug(dcTempo()) << "Post setup thing" << thing->name();
+
+ if (!m_pluginTimer15min) {
+ m_pluginTimer15min = hardwareManager()->pluginTimerManager()->registerTimer(60*15);
+ connect(m_pluginTimer15min, &PluginTimer::timeout, this, [this]() {
+ qCDebug(dcTempo()) << "Refresh timer timout, polling all Tempo accounts.";
+ Q_FOREACH (Thing *thing, myThings().filterByThingClassId(tempoConnectionThingClassId)) {
+ Tempo *tempo = m_tempoConnections.value(thing->id());
+ if (!tempo) {
+ qWarning(dcTempo()) << "No Tempo connection found for" << thing->name();
+ continue;
+ }
+ tempo->getAccounts();
+ tempo->getTeams();
+ Q_FOREACH (Thing *childThing, myThings().filterByParentId(thing->id())) {
+ if (childThing->thingClassId() == accountThingClassId) {
+ QString key = childThing->paramValue(accountThingKeyParamTypeId).toString();
+ tempo->getWorkloadByAccount(key, QDate(1970, 1, 1), QDate::currentDate(), 0, 1000);
+ } else if (childThing->thingClassId() == teamThingClassId) {
+ int id = childThing->paramValue(teamThingIdParamTypeId).toInt();
+ tempo->getWorkloadByTeam(id, QDate(1970, 1, 1), QDate::currentDate(), 0, 1000);
+ }
+ }
+ }
+ });
+ }
+
+ if (thing->thingClassId() == tempoConnectionThingClassId) {
+ Tempo *tempo = m_tempoConnections.value(thing->id());
+ if (!tempo) {
+ qCWarning(dcTempo()) << "Tempo connection not found for" << thing->name();
+ return;
+ }
+ tempo->getAccounts();
+
+ } else if (thing->thingClassId() == accountThingClassId) {
+ Tempo *tempo = m_tempoConnections.value(thing->parentId());
+ QString key = thing->paramValue(accountThingKeyParamTypeId).toString();
+ QDate from(1970, 1, 1);
+ tempo->getWorkloadByAccount(key, from, QDate::currentDate(), 0, 1000);
+ tempo->getAccounts();
+ } else if (thing->thingClassId() == teamThingClassId) {
+ Tempo *tempo = m_tempoConnections.value(thing->parentId());
+ int id = thing->paramValue(teamThingIdParamTypeId).toInt();
+ QDate from(1970, 1, 1);
+ tempo->getWorkloadByTeam(id, from, QDate::currentDate(), 0, 1000);
+ tempo->getTeams();
+ }
+}
+
+void IntegrationPluginTempo::thingRemoved(Thing *thing)
+{
+ qCDebug(dcTempo()) << "Thing removed" << thing->name();
+ if (thing->thingClassId() == tempoConnectionThingClassId) {
+ m_tempoConnections.take(thing->id())->deleteLater();
+ } else if (thing->thingClassId() == teamThingClassId ||
+ thing->thingClassId() == accountThingClassId) {
+ m_worklogBuffer.remove(thing->id());
+ }
+
+ if (myThings().isEmpty()) {
+ qCDebug(dcTempo()) << "Stopping plugin timer";
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer15min);
+ m_pluginTimer15min = nullptr;
+ }
+}
+
+void IntegrationPluginTempo::onConnectionChanged(bool connected)
+{
+ Tempo *tempo = static_cast(sender());
+ Thing *thing = myThings().findById(m_tempoConnections.key(tempo));
+ if (!thing)
+ return;
+ thing->setStateValue(tempoConnectionConnectedStateTypeId, connected);
+ if (!connected) {
+ Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) {
+ child->setStateValue(accountConnectedStateTypeId, connected);
+ }
+ }
+}
+
+void IntegrationPluginTempo::onAuthenticationStatusChanged(bool authenticated)
+{
+ qCDebug(dcTempo()) << "Authentication changed" << authenticated;
+
+ Tempo *tempo = static_cast(sender());
+ Thing *thing = myThings().findById(m_tempoConnections.key(tempo));
+ if (!thing)
+ return;
+
+ thing->setStateValue(tempoConnectionLoggedInStateTypeId, authenticated);
+}
+
+void IntegrationPluginTempo::onAccountsReceived(const QList accounts)
+{
+ qCDebug(dcTempo()) << "Accounts received";
+
+ Q_FOREACH(Tempo::Account account, accounts) {
+ // qCDebug(dcTempo()) << " - Account" << account.name;
+ // qCDebug(dcTempo()) << " - Key" << account.key;
+ // qCDebug(dcTempo()) << " - Monthly budget" << account.monthlyBudget;
+ // qCDebug(dcTempo()) << " - Lead" << account.lead.displayName;
+ // qCDebug(dcTempo()) << " - Is Global" << account.global;
+ // qCDebug(dcTempo()) << " - Contact type" << account.contact.type;
+ // qCDebug(dcTempo()) << " - Contact account id" << account.contact.accountId;
+ // qCDebug(dcTempo()) << " - Contact" << account.contact.displayName;
+ // qCDebug(dcTempo()) << " - Category Id" << account.category.id;
+ // qCDebug(dcTempo()) << " - Category name" << account.category.name;
+ // qCDebug(dcTempo()) << " - Category key" << account.category.key;
+ // qCDebug(dcTempo()) << " - Customer id" << account.customer.id;
+ // qCDebug(dcTempo()) << " - Customer key" << account.customer.key;
+ // qCDebug(dcTempo()) << " - Customer name" << account.customer.name;
+
+ Thing *thing = myThings().findByParams(ParamList() << Param(accountThingKeyParamTypeId, account.key));
+ if (!thing) {
+ continue;
+ }
+ thing->setName(account.name);
+ thing->setStateValue(accountConnectedStateTypeId, (account.status != Tempo::Status::Archived));
+ thing->setStateValue(accountLeadStateTypeId, account.lead.displayName);
+ thing->setStateValue(accountGlobalStateTypeId, account.global);
+ thing->setStateValue(accountCategoryStateTypeId, account.category.name);
+ thing->setStateValue(accountCustomerStateTypeId, account.customer.name);
+ thing->setStateValue(accountContactStateTypeId, account.contact.displayName);
+ thing->setStateValue(accountMonthlyBudgetStateTypeId, account.monthlyBudget);
+
+ if (account.status == Tempo::Status::Open) {
+ thing->setStateValue(accountStatusStateTypeId, "Open");
+ } else if (account.status == Tempo::Status::Closed) {
+ thing->setStateValue(accountStatusStateTypeId, "Closed");
+ } else if (account.status == Tempo::Status::Archived) {
+ thing->setStateValue(accountStatusStateTypeId, "Archived");
+ }
+ }
+}
+
+void IntegrationPluginTempo::onTeamsReceived(const QList teams)
+{
+ qCDebug(dcTempo()) << "Teams received";
+
+ Q_FOREACH(Tempo::Team team, teams) {
+ Thing *thing = myThings().findByParams(ParamList() << Param(teamThingIdParamTypeId, team.id));
+ if (!thing) {
+ continue;
+ }
+ thing->setName(team.name);
+ thing->setStateValue(teamConnectedStateTypeId, true);
+ thing->setStateValue(teamLeadStateTypeId, team.lead.displayName);
+ }
+}
+
+void IntegrationPluginTempo::onAccountWorkloadReceived(const QString &accountKey, QList workloads, int limit, int offset)
+{
+ qCDebug(dcTempo()) << "Account workload received, account key:" << accountKey << "Worklog etries: "<< workloads.count();
+ Thing *thing = myThings().findByParams(ParamList() << Param(accountThingKeyParamTypeId, accountKey));
+ if (!thing) {
+ qCWarning(dcTempo()) << "Could not find account thing for account key" << accountKey;
+ return;
+ }
+
+ if (offset == 0) {
+ m_worklogBuffer.remove(thing->id());
+ }
+ if (workloads.count() >= limit) {
+ //limit is reached
+ if (m_worklogBuffer.contains(thing->id())) {
+ m_worklogBuffer[thing->id()].append(workloads);
+ } else {
+ m_worklogBuffer.insert(thing->id(), workloads);
+ }
+ Tempo *tempo = m_tempoConnections.value(thing->parentId());
+ if (tempo) {
+ tempo->getWorkloadByAccount(accountKey, QDate(1970, 1, 1), QDate::currentDate(), offset+workloads.count(), limit);
+ }
+
+ } else {
+ uint totalTimeSpentSeconds = 0;
+ uint thisMonthTimeSpentSeconds = 0;
+ QDate today = QDate::currentDate();
+ Q_FOREACH(Tempo::Worklog workload, workloads) {
+ if ((workload.startDate.month() == today.month()) && (workload.startDate.year() == today.year())) {
+ thisMonthTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ totalTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ if (m_worklogBuffer.contains(thing->id())) {
+ Q_FOREACH(Tempo::Worklog workload, m_worklogBuffer.take(thing->id())) {
+ if ((workload.startDate.month() == today.month()) && (workload.startDate.year() == today.year())) {
+ thisMonthTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ totalTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ }
+ thing->setStateValue(accountTotalTimeSpentStateTypeId, totalTimeSpentSeconds/3600.00);
+ thing->setStateValue(accountMonthTimeSpentStateTypeId, thisMonthTimeSpentSeconds/3600.00);
+ }
+}
+
+void IntegrationPluginTempo::onTeamWorkloadReceived(int teamId, QList workloads, int limit, int offset)
+{
+ qCDebug(dcTempo()) << "Team workload received, team ID:" << teamId << "Worklog entries: "<< workloads.count();
+ Thing *thing = myThings().findByParams(ParamList() << Param(teamThingIdParamTypeId, teamId));
+ if (!thing) {
+ qCWarning(dcTempo()) << "Could not find team thing for account key" << teamId;
+ return;
+ }
+ if (offset == 0) {
+ m_worklogBuffer.remove(thing->id());
+ }
+ if (workloads.count() >= limit) {
+ //limit is reached#
+ if (m_worklogBuffer.contains(thing->id())) {
+ m_worklogBuffer[thing->id()].append(workloads);
+ } else {
+ m_worklogBuffer.insert(thing->id(), workloads);
+ }
+ Tempo *tempo = m_tempoConnections.value(thing->parentId());
+ if (tempo) {
+ tempo->getWorkloadByTeam(teamId, QDate(1970, 1, 1), QDate::currentDate(), offset+workloads.count(), limit);
+ }
+
+ } else {
+ uint totalTimeSpentSeconds = 0;
+ uint thisMonthTimeSpentSeconds = 0;
+ QDate today = QDate::currentDate();
+ Q_FOREACH(Tempo::Worklog workload, workloads) {
+ if ((workload.startDate.month() == today.month()) && (workload.startDate.year() == today.year())) {
+ thisMonthTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ totalTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ if (m_worklogBuffer.contains(thing->id())) {
+ Q_FOREACH(Tempo::Worklog workload, m_worklogBuffer.take(thing->id())) {
+ if ((workload.startDate.month() == today.month()) && (workload.startDate.year() == today.year())) {
+ thisMonthTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ totalTimeSpentSeconds += workload.timeSpentSeconds;
+ }
+ }
+ thing->setStateValue(teamTotalTimeSpentStateTypeId, totalTimeSpentSeconds/3600.00);
+ thing->setStateValue(teamMonthTimeSpentStateTypeId, thisMonthTimeSpentSeconds/3600.00);
+ }
+}
+
diff --git a/tempo/integrationplugintempo.h b/tempo/integrationplugintempo.h
new file mode 100644
index 00000000..17d73b22
--- /dev/null
+++ b/tempo/integrationplugintempo.h
@@ -0,0 +1,74 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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 discoverThings(ThingDiscoveryInfo *info) override;
+ void setupThing(ThingSetupInfo *info) override;
+ void postSetupThing(Thing *thing) override;
+ void thingRemoved(Thing *thing) override;
+
+private:
+ PluginTimer *m_pluginTimer15min = nullptr;
+
+ QHash> m_worklogBuffer;
+ QHash m_setupTempoConnections;
+ QHash m_tempoConnections;
+
+private slots:
+ void onConnectionChanged(bool connected);
+ void onAuthenticationStatusChanged(bool authenticated);
+
+ void onAccountsReceived(const QList accounts);
+ void onTeamsReceived(const QList teams);
+
+ void onAccountWorkloadReceived(const QString &accountKey, QList workloads, int limit, int offset);
+ void onTeamWorkloadReceived(int teamId, QList workloads, int limit, int offset);
+};
+#endif // INTEGRATIONPLUGINTEMPO_H
diff --git a/tempo/integrationplugintempo.json b/tempo/integrationplugintempo.json
new file mode 100644
index 00000000..4040a3df
--- /dev/null
+++ b/tempo/integrationplugintempo.json
@@ -0,0 +1,211 @@
+{
+ "id": "809bc4ca-d1cd-4279-9e0d-7324537ccb5a",
+ "name": "tempo",
+ "displayName": "Tempo",
+ "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": "displayPin",
+ "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": "8be71352-bdfd-450b-903e-79a4ed203701",
+ "name": "account",
+ "displayName": "Account",
+ "interfaces": ["connectable"],
+ "createMethods": ["discovery"],
+ "settingsTypes": [
+ {
+ "id": "56c460b2-37d8-453a-b4f4-8e58be348f85",
+ "name": "startDate",
+ "displayName": "Start date",
+ "defaultValue": "",
+ "type": "QDate"
+ }
+ ],
+ "paramTypes": [
+ {
+ "id": "c6aeddae-56af-496d-a419-1635ff9bae50",
+ "name": "key",
+ "displayName": "Key",
+ "defaultValue": "",
+ "type": "QString",
+ "readOnly": true
+ }
+ ],
+ "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"
+ },
+ {
+ "id": "1ac39002-56a1-4911-aa68-9d14e142edae",
+ "name": "totalTimeSpent",
+ "displayName": "Total time spent",
+ "displayNameEvent": "Total time spent changed",
+ "defaultValue": 0,
+ "type": "double",
+ "unit": "Hours"
+ },
+ {
+ "id": "81bec4e8-9fd3-43d1-b339-2a7fdd83e8cb",
+ "name": "monthTimeSpent",
+ "displayName": "This month time spent",
+ "displayNameEvent": "This month time spent changed",
+ "defaultValue": 0,
+ "type": "double",
+ "unit": "Hours"
+ }
+ ]
+ },
+ {
+ "id": "11c85176-e7fe-44b4-995a-24757273f3af",
+ "name": "team",
+ "displayName": "Team",
+ "interfaces": ["connectable"],
+ "createMethods": ["discovery"],
+ "paramTypes": [
+ {
+ "id": "bb90e986-fcfa-47e8-8783-f2b5a887314a",
+ "name": "id",
+ "displayName": "Id",
+ "defaultValue": 0,
+ "type": "int",
+ "readOnly": true
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "a125d3b5-676f-49eb-bb93-feae233c2e91",
+ "name": "connected",
+ "displayName": "Connected",
+ "displayNameEvent": "Connected changed",
+ "defaultValue": true,
+ "cached": false,
+ "type": "bool"
+ },
+ {
+ "id": "667a9d8d-4e80-4c7c-938c-d698853fa4b1",
+ "name": "lead",
+ "displayName": "Lead",
+ "displayNameEvent": "Lead changed",
+ "defaultValue": "",
+ "type": "QString"
+ },
+ {
+ "id": "a694682e-3c2a-4146-aa56-9e75fd82bcab",
+ "name": "totalTimeSpent",
+ "displayName": "Total time spent",
+ "displayNameEvent": "Total time spent changed",
+ "defaultValue": 0,
+ "type": "double",
+ "unit": "Hours"
+ },
+ {
+ "id": "f5ec7b30-3074-41e9-b1fc-62b6307ddbe1",
+ "name": "monthTimeSpent",
+ "displayName": "This month time spent",
+ "displayNameEvent": "This month time spent changed",
+ "defaultValue": 0,
+ "type": "double",
+ "unit": "Hours"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tempo/meta.json b/tempo/meta.json
new file mode 100644
index 00000000..ed17d2af
--- /dev/null
+++ b/tempo/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "Tempo",
+ "tagline": "Get logged hours from Tempo.",
+ "icon": "tempo.svg",
+ "stability": "consumer",
+ "offline": false,
+ "technologies": [
+ "cloud"
+ ],
+ "categories": [
+ "online-service"
+ ]
+}
diff --git a/tempo/tempo.cpp b/tempo/tempo.cpp
new file mode 100644
index 00000000..93f40062
--- /dev/null
+++ b/tempo/tempo.cpp
@@ -0,0 +1,353 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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 QString &token, QObject *parent) :
+ QObject(parent),
+ m_token(token),
+ m_networkManager(networkmanager)
+{
+ qCDebug(dcTempo()) << "Creating tempo connection";
+}
+
+Tempo::~Tempo()
+{
+ qCDebug(dcTempo()) << "Deleting tempo connection";
+}
+
+QString Tempo::token() const
+{
+ return m_token;
+}
+
+void Tempo::getTeams()
+{
+ QUrl url = QUrl(m_baseControlUrl+"/teams");
+ qCDebug(dcTempo()) << "Get teams, url" << url.toString();
+
+ QNetworkRequest request(url);
+ request.setRawHeader("Authorization", "Bearer "+m_token.toUtf8());
+
+ 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 teamList = dataMap.value("results").toList();
+ QList teams;
+ Q_FOREACH(QVariant var, teamList) {
+ QVariantMap map = var.toMap();
+ Team team;
+ team.self = map["self"].toString();
+ team.id = map["id"].toInt();
+ team.name = map["name"].toString();
+ team.summary = map["summery"].toString();
+
+ QVariantMap lead = map["lead"].toMap();
+ team.lead.self = lead["self"].toString();
+ team.lead.accountId = lead["accountId"].toString();
+ team.lead.displayName = lead["displayName"].toString();
+
+ teams.append(team);
+ }
+ if (!teams.isEmpty()) {
+ emit teamsReceived(teams);
+ }
+ });
+}
+
+void Tempo::getAccounts()
+{
+ QUrl url = QUrl(m_baseControlUrl+"/accounts");
+ qCDebug(dcTempo()) << "Get accounts. Url" << url.toString();
+
+ QNetworkRequest request(url);
+ request.setRawHeader("Authorization", "Bearer "+m_token.toUtf8());
+
+ 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();
+
+ QVariantMap customer = map["customer"].toMap();
+ account.customer.self = customer["self"].toString();
+ account.customer.key = customer["key"].toString();
+ account.customer.id = customer["id"].toInt();
+ account.customer.name = customer["name"].toString();
+
+ QVariantMap category = map["category"].toMap();
+ account.category.self = category["self"].toString();
+ account.category.key = category["key"].toString();
+ account.category.name = category["name"].toString();
+ account.category.id = category["id"].toInt();
+
+ QVariantMap contact = map["contact"].toMap();
+ account.contact.self = contact["self"].toString();
+ account.contact.type = contact["type"].toString();
+ account.contact.accountId = contact["accountId"].toString();
+ account.contact.displayName = contact["displayName"].toString();
+
+ accounts.append(account);
+ }
+ if (!accounts.isEmpty()) {
+ emit accountsReceived(accounts);
+ }
+ });
+}
+
+void Tempo::getWorkloadByAccount(const QString &accountKey, QDate from, QDate to, int offset, int limit)
+{
+ QUrl url = QUrl(m_baseControlUrl+"/worklogs/account/"+accountKey);
+ QUrlQuery query;
+ query.addQueryItem("from", from.toString(Qt::DateFormat::ISODate));
+ query.addQueryItem("to", to.toString(Qt::DateFormat::ISODate));
+ query.addQueryItem("offset", QString::number(offset));
+ query.addQueryItem("limit", QString::number(limit));
+ url.setQuery(query);
+
+ qCDebug(dcTempo()) << "Get workload by account. Url" << url.toString();
+
+ QNetworkRequest request(url);
+ request.setRawHeader("Authorization", "Bearer "+m_token.toUtf8());
+
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [this, accountKey, reply]{
+
+ QByteArray rawData = reply->readAll();
+ if (!checkStatusCode(reply, rawData)) {
+ return;
+ }
+ QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap();
+ int offset = dataMap.value("metadata").toMap().value("offset").toInt();
+ int limit = dataMap.value("metadata").toMap().value("limit").toInt();
+ QList worklogs = parseJsonForWorklog(dataMap);
+ if (!worklogs.isEmpty())
+ emit accountWorklogsReceived(accountKey, worklogs, limit, offset);
+ });
+}
+
+void Tempo::getWorkloadByTeam(int teamId, QDate from, QDate to, int offset, int limit)
+{
+ QUrl url = QUrl(m_baseControlUrl+"/worklogs/team/"+QString::number(teamId));
+ QUrlQuery query;
+ query.addQueryItem("from", from.toString(Qt::DateFormat::ISODate));
+ query.addQueryItem("to", to.toString(Qt::DateFormat::ISODate));
+ query.addQueryItem("offset", QString::number(offset));
+ query.addQueryItem("limit", QString::number(limit));
+ url.setQuery(query);
+
+ qCDebug(dcTempo()) << "Get workload by account. Url" << url.toString();
+
+ QNetworkRequest request(url);
+ request.setRawHeader("Authorization", "Bearer "+m_token.toUtf8());
+
+ QNetworkReply *reply = m_networkManager->get(request);
+ connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
+ connect(reply, &QNetworkReply::finished, this, [this, teamId, reply]{
+ QByteArray rawData = reply->readAll();
+ if (!checkStatusCode(reply, rawData)) {
+ return;
+ }
+ QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap();
+ int offset = dataMap.value("metadata").toMap().value("offset").toInt();
+ int limit = dataMap.value("metadata").toMap().value("limit").toInt();
+ QList worklogs = parseJsonForWorklog(dataMap);
+ if (!worklogs.isEmpty())
+ emit teamWorklogsReceived(teamId, worklogs, limit, offset);
+ });
+}
+
+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);
+ }
+}
+
+QList Tempo::parseJsonForWorklog(const QVariantMap &data)
+{
+ QVariantList worklogList = data.value("results").toList();
+ qCDebug(dcTempo()) << "Worklog received";
+ qCDebug(dcTempo()) << " - Count:" << data.value("metadata").toMap().value("count");
+ qCDebug(dcTempo()) << " - Offset:" << data.value("metadata").toMap().value("offset");
+ qCDebug(dcTempo()) << " - Limit:" << data.value("metadata").toMap().value("limit");
+
+ QList worklogs;
+ Q_FOREACH(QVariant var, worklogList) {
+ QVariantMap map = var.toMap();
+ Worklog worklog;
+ worklog.self = map["self"].toString();
+ worklog.tempoWorklogId = map["tempoWorklogId"].toInt();
+ worklog.jiraWorklogId = map["jiraWorklogId"].toInt();
+ worklog.issue = map["issue"].toMap().value("key").toString();
+ worklog.timeSpentSeconds = map["timeSpentSeconds"].toInt();
+ worklog.startDate = QDate::fromString(map["startDate"].toString(), Qt::ISODate);
+ worklog.startTime = QTime::fromString(map["startTime"].toString(), Qt::ISODate);
+ worklog.description = map["description"].toString();
+ worklog.createdAt = QDateTime::fromString(map["createdAt"].toString(), Qt::ISODate);
+ worklog.updatedAt = QDateTime::fromString(map["updatedAt"].toString(), Qt::ISODate);
+ worklog.authorAccountId = map["author"].toMap().value("accountId").toString();
+ worklog.authorDisplayName = map["author"].toMap().value("displayName").toString();
+ worklogs.append(worklog);
+ }
+ return worklogs;
+}
+
+bool Tempo::checkStatusCode(QNetworkReply *reply, const QByteArray &rawData)
+{
+ // Check for the internet connection
+ if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError ||
+ reply->error() == QNetworkReply::NetworkError::UnknownNetworkError ||
+ reply->error() == QNetworkReply::NetworkError::TemporaryNetworkFailureError) {
+ qCWarning(dcTempo()) << "Connection error" << reply->errorString();
+ setConnected(false);
+ setAuthenticated(false);
+ return false;
+ } else {
+ setConnected(true);
+ }
+ int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ QJsonParseError error;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData, &error);
+
+ switch (status){
+ case 200: //The request was successful. Typically returned for successful GET requests.
+ case 204: //The request was successful. Typically returned for successful PUT/DELETE requests with no payload.
+ break;
+ case 400: //Error occurred (e.g. validation error - value is out of range)
+ if(!jsonDoc.toVariant().toMap().contains("error")) {
+ if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_client") {
+ qWarning(dcTempo()) << "Client token provided doesn’t correspond to client that generated auth code.";
+ }
+ if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_redirect_uri") {
+ qWarning(dcTempo()) << "Missing redirect_uri parameter.";
+ }
+ if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_code") {
+ qWarning(dcTempo()) << "Expired authorization code.";
+ }
+ }
+ setAuthenticated(false);
+ return false;
+ case 401:
+ qWarning(dcTempo()) << "Client does not have permission to use this API.";
+ setAuthenticated(false);
+ return false;
+ case 403:
+ qCWarning(dcTempo()) << "Forbidden, Scope has not been granted or home appliance is not assigned to HC account";
+ setAuthenticated(false);
+ return false;
+ case 404:
+ qCWarning(dcTempo()) << "Not Found. This resource is not available (e.g. no images on washing machine)";
+ return false;
+ case 405:
+ qWarning(dcTempo()) << "Wrong HTTP method used.";
+ setAuthenticated(false);
+ return false;
+ case 408:
+ qCWarning(dcTempo())<< "Request Timeout, API Server failed to produce an answer or has no connection to backend service";
+ return false;
+ case 409:
+ qCWarning(dcTempo()) << "Conflict - Command/Query cannot be executed for the home appliance, the error response contains the error details";
+ qCWarning(dcTempo()) << "Error" << jsonDoc;
+ return false;
+ case 415:
+ qCWarning(dcTempo())<< "Unsupported Media Type. The request's Content-Type is not supported";
+ return false;
+ case 429:
+ qCWarning(dcTempo())<< "Too Many Requests, the number of requests for a specific endpoint exceeded the quota of the client";
+ return false;
+ case 500:
+ qCWarning(dcTempo())<< "Internal Server Error, in case of a server configuration error or any errors in resource files";
+ return false;
+ case 503:
+ qCWarning(dcTempo())<< "Service Unavailable,if a required backend service is not available";
+ return false;
+ default:
+ break;
+ }
+
+ if (error.error != QJsonParseError::NoError) {
+ qCWarning(dcTempo()) << "Received invalide JSON object" << rawData;
+ qCWarning(dcTempo()) << "Status" << status;
+ setAuthenticated(false);
+ return false;
+ }
+
+ setAuthenticated(true);
+ return true;
+}
diff --git a/tempo/tempo.h b/tempo/tempo.h
new file mode 100644
index 00000000..f3a24a02
--- /dev/null
+++ b/tempo/tempo.h
@@ -0,0 +1,151 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* 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 key;
+ int id;
+ QString name;
+ };
+
+ 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;
+ };
+
+ struct Worklog {
+ QUrl self;
+ int tempoWorklogId;
+ int jiraWorklogId;
+ QString issue;
+ int timeSpentSeconds;
+ QDate startDate;
+ QTime startTime;
+ QString description;
+ QDateTime createdAt;
+ QDateTime updatedAt;
+ QString authorAccountId;
+ QString authorDisplayName;
+ };
+
+ struct Team {
+ QUrl self;
+ int id;
+ QString name;
+ QString summary;
+ Lead lead;
+ };
+
+ explicit Tempo(NetworkAccessManager *networkmanager, const QString &token, QObject *parent = nullptr);
+ ~Tempo() override;
+ QString token() const;
+
+ void getTeams();
+ void getAccounts();
+ void getWorkloadByAccount(const QString &accountKey, QDate from, QDate to, int offset = 0, int limit = 50);
+ void getWorkloadByTeam(int teamId, QDate from, QDate to, int offset = 0, int limit = 50);
+
+private:
+ QByteArray m_baseControlUrl = "https://api.tempo.io/core/3";
+ QString m_token;
+
+ NetworkAccessManager *m_networkManager = nullptr;
+
+ void setAuthenticated(bool state);
+ void setConnected(bool state);
+
+ bool m_authenticated = false;
+ bool m_connected = false;
+
+ QList parseJsonForWorklog(const QVariantMap &data);
+ bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData);
+
+private slots:
+
+signals:
+ void authenticationStatusChanged(bool state);
+ void connectionChanged(bool connected);
+
+ void teamsReceived(const QList teams);
+ void accountsReceived(const QList accounts);
+ void accountWorklogsReceived(const QString &accountKey, QList worklogs, int limit, int offset);
+ void teamWorklogsReceived(int teamId, QList worklogs, int limit, int offset);
+};
+
+#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
+
diff --git a/tempo/translations/809bc4ca-d1cd-4279-9e0d-7324537ccb5a-en_US.ts b/tempo/translations/809bc4ca-d1cd-4279-9e0d-7324537ccb5a-en_US.ts
new file mode 100644
index 00000000..6e5bf7fd
--- /dev/null
+++ b/tempo/translations/809bc4ca-d1cd-4279-9e0d-7324537ccb5a-en_US.ts
@@ -0,0 +1,285 @@
+
+
+
+
+ IntegrationPluginTempo
+
+
+ Please enter your Tempo API integration token.
+
+
+
+
+ Tempo server not reachable, please check the internet connection.
+
+
+
+
+ Create a Tempo connection first
+
+
+
+
+ Token is not available.
+
+
+
+
+ tempo
+
+
+ Account
+ The name of the ThingClass ({8be71352-bdfd-450b-903e-79a4ed203701})
+
+
+
+
+
+ Category
+ The name of the ParamType (ThingClass: account, EventType: category, ID: {3af6d1c0-bb0a-406f-809b-2c367e1a16bb})
+----------
+The name of the StateType ({3af6d1c0-bb0a-406f-809b-2c367e1a16bb}) of ThingClass account
+
+
+
+
+ Category changed
+ The name of the EventType ({3af6d1c0-bb0a-406f-809b-2c367e1a16bb}) of ThingClass account
+
+
+
+
+
+
+
+
+
+ Connected
+ The name of the ParamType (ThingClass: team, EventType: connected, ID: {a125d3b5-676f-49eb-bb93-feae233c2e91})
+----------
+The name of the StateType ({a125d3b5-676f-49eb-bb93-feae233c2e91}) of ThingClass team
+----------
+The name of the ParamType (ThingClass: account, EventType: connected, ID: {0b776bc1-9e56-4205-9bc3-b356026f5b64})
+----------
+The name of the StateType ({0b776bc1-9e56-4205-9bc3-b356026f5b64}) of ThingClass account
+----------
+The name of the ParamType (ThingClass: tempoConnection, EventType: connected, ID: {15f45315-5419-4e1b-ace3-fc21503d3b70})
+----------
+The name of the StateType ({15f45315-5419-4e1b-ace3-fc21503d3b70}) of ThingClass tempoConnection
+
+
+
+
+
+
+ Connected changed
+ The name of the EventType ({a125d3b5-676f-49eb-bb93-feae233c2e91}) of ThingClass team
+----------
+The name of the EventType ({0b776bc1-9e56-4205-9bc3-b356026f5b64}) of ThingClass account
+----------
+The name of the EventType ({15f45315-5419-4e1b-ace3-fc21503d3b70}) of ThingClass tempoConnection
+
+
+
+
+
+ Contact
+ The name of the ParamType (ThingClass: account, EventType: contact, ID: {ece43b12-4a0d-4e25-b811-b1aca610bea8})
+----------
+The name of the StateType ({ece43b12-4a0d-4e25-b811-b1aca610bea8}) of ThingClass account
+
+
+
+
+ Contact changed
+ The name of the EventType ({ece43b12-4a0d-4e25-b811-b1aca610bea8}) of ThingClass account
+
+
+
+
+
+ Customer
+ The name of the ParamType (ThingClass: account, EventType: Customer, ID: {3dcc1426-51f8-46fa-9967-5a93d7bb2633})
+----------
+The name of the StateType ({3dcc1426-51f8-46fa-9967-5a93d7bb2633}) of ThingClass account
+
+
+
+
+ Customer changed
+ The name of the EventType ({3dcc1426-51f8-46fa-9967-5a93d7bb2633}) of ThingClass account
+
+
+
+
+
+ Global
+ The name of the ParamType (ThingClass: account, EventType: global, ID: {abd55ea0-ad4e-413e-bc77-3e8b7f0a9be4})
+----------
+The name of the StateType ({abd55ea0-ad4e-413e-bc77-3e8b7f0a9be4}) of ThingClass account
+
+
+
+
+ Global changed
+ The name of the EventType ({abd55ea0-ad4e-413e-bc77-3e8b7f0a9be4}) of ThingClass account
+
+
+
+
+ Id
+ The name of the ParamType (ThingClass: team, Type: thing, ID: {bb90e986-fcfa-47e8-8783-f2b5a887314a})
+
+
+
+
+ Key
+ The name of the ParamType (ThingClass: account, Type: thing, ID: {c6aeddae-56af-496d-a419-1635ff9bae50})
+
+
+
+
+
+
+
+ Lead
+ The name of the ParamType (ThingClass: team, EventType: lead, ID: {667a9d8d-4e80-4c7c-938c-d698853fa4b1})
+----------
+The name of the StateType ({667a9d8d-4e80-4c7c-938c-d698853fa4b1}) of ThingClass team
+----------
+The name of the ParamType (ThingClass: account, EventType: lead, ID: {f1f2af66-d09a-4242-9058-401145f662c4})
+----------
+The name of the StateType ({f1f2af66-d09a-4242-9058-401145f662c4}) of ThingClass account
+
+
+
+
+
+ Lead changed
+ The name of the EventType ({667a9d8d-4e80-4c7c-938c-d698853fa4b1}) of ThingClass team
+----------
+The name of the EventType ({f1f2af66-d09a-4242-9058-401145f662c4}) of ThingClass account
+
+
+
+
+
+ Logged in
+ The name of the ParamType (ThingClass: tempoConnection, EventType: loggedIn, ID: {e4b5be87-dbc9-481e-88da-608c71be8bda})
+----------
+The name of the StateType ({e4b5be87-dbc9-481e-88da-608c71be8bda}) of ThingClass tempoConnection
+
+
+
+
+ Logged in changed
+ The name of the EventType ({e4b5be87-dbc9-481e-88da-608c71be8bda}) of ThingClass tempoConnection
+
+
+
+
+
+ Monthly budget
+ The name of the ParamType (ThingClass: account, EventType: monthlyBudget, ID: {44ebbc18-7511-48c0-860b-c4de5f634ed6})
+----------
+The name of the StateType ({44ebbc18-7511-48c0-860b-c4de5f634ed6}) of ThingClass account
+
+
+
+
+ Monthly budget changed
+ The name of the EventType ({44ebbc18-7511-48c0-860b-c4de5f634ed6}) of ThingClass account
+
+
+
+
+ Start date
+ The name of the ParamType (ThingClass: account, Type: settings, ID: {56c460b2-37d8-453a-b4f4-8e58be348f85})
+
+
+
+
+
+ Status
+ The name of the ParamType (ThingClass: account, EventType: status, ID: {7948f15b-7243-404e-9e67-18e915e8b328})
+----------
+The name of the StateType ({7948f15b-7243-404e-9e67-18e915e8b328}) of ThingClass account
+
+
+
+
+ Status changed
+ The name of the EventType ({7948f15b-7243-404e-9e67-18e915e8b328}) of ThingClass account
+
+
+
+
+ Team
+ The name of the ThingClass ({11c85176-e7fe-44b4-995a-24757273f3af})
+
+
+
+
+
+ Tempo
+ The name of the vendor ({58fc1ab7-b8b5-4e52-8388-72957ce5852d})
+----------
+The name of the plugin tempo ({809bc4ca-d1cd-4279-9e0d-7324537ccb5a})
+
+
+
+
+ Tempo connection
+ The name of the ThingClass ({878eae0a-6217-4b36-bd46-72c911e52e73})
+
+
+
+
+
+
+
+ This month time spent
+ The name of the ParamType (ThingClass: team, EventType: monthTimeSpent, ID: {f5ec7b30-3074-41e9-b1fc-62b6307ddbe1})
+----------
+The name of the StateType ({f5ec7b30-3074-41e9-b1fc-62b6307ddbe1}) of ThingClass team
+----------
+The name of the ParamType (ThingClass: account, EventType: monthTimeSpent, ID: {81bec4e8-9fd3-43d1-b339-2a7fdd83e8cb})
+----------
+The name of the StateType ({81bec4e8-9fd3-43d1-b339-2a7fdd83e8cb}) of ThingClass account
+
+
+
+
+
+ This month time spent changed
+ The name of the EventType ({f5ec7b30-3074-41e9-b1fc-62b6307ddbe1}) of ThingClass team
+----------
+The name of the EventType ({81bec4e8-9fd3-43d1-b339-2a7fdd83e8cb}) of ThingClass account
+
+
+
+
+
+
+
+ Total time spent
+ The name of the ParamType (ThingClass: team, EventType: totalTimeSpent, ID: {a694682e-3c2a-4146-aa56-9e75fd82bcab})
+----------
+The name of the StateType ({a694682e-3c2a-4146-aa56-9e75fd82bcab}) of ThingClass team
+----------
+The name of the ParamType (ThingClass: account, EventType: totalTimeSpent, ID: {1ac39002-56a1-4911-aa68-9d14e142edae})
+----------
+The name of the StateType ({1ac39002-56a1-4911-aa68-9d14e142edae}) of ThingClass account
+
+
+
+
+
+ Total time spent changed
+ The name of the EventType ({a694682e-3c2a-4146-aa56-9e75fd82bcab}) of ThingClass team
+----------
+The name of the EventType ({1ac39002-56a1-4911-aa68-9d14e142edae}) of ThingClass account
+
+
+
+