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 + + + +