Merge PR #400: New Plugin: Tempo
This commit is contained in:
commit
bdd41d3091
10
debian/control
vendored
10
debian/control
vendored
@ -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,
|
||||
|
||||
1
debian/nymea-plugin-tempo.install.in
vendored
Normal file
1
debian/nymea-plugin-tempo.install.in
vendored
Normal file
@ -0,0 +1 @@
|
||||
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationplugintempo.so
|
||||
@ -166,6 +166,7 @@ private:
|
||||
bool m_connected = false;
|
||||
|
||||
bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData);
|
||||
|
||||
private slots:
|
||||
void onRefreshTimeout();
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ PLUGIN_DIRS = \
|
||||
tasmota \
|
||||
tcpcommander \
|
||||
telegram \
|
||||
tempo \
|
||||
texasinstruments \
|
||||
tplink \
|
||||
tuya \
|
||||
|
||||
17
tempo/README.md
Normal file
17
tempo/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Tempo
|
||||
|
||||
Add Tempo time tracking accounts to nymea.
|
||||
|
||||
## Supported Things
|
||||
|
||||
* Tempo Atlassian Account
|
||||
* Time Accounts
|
||||
|
||||
## Requirements
|
||||
|
||||
* Package 'nymea-plugin-tempo' must be installed
|
||||
* A atlassian account and client credentials
|
||||
|
||||
## More
|
||||
|
||||
https://apidocs.tempo.io/
|
||||
466
tempo/integrationplugintempo.cpp
Normal file
466
tempo/integrationplugintempo.cpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* For any further details and any questions please contact us under
|
||||
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||
* https://nymea.io/license/faq
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include "integrationplugintempo.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include "network/networkaccessmanager.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
#include <QUrl>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
|
||||
IntegrationPluginTempo::IntegrationPluginTempo()
|
||||
{
|
||||
}
|
||||
|
||||
void IntegrationPluginTempo::startPairing(ThingPairingInfo *info)
|
||||
{
|
||||
qCDebug(dcTempo()) << "Start pairing";
|
||||
|
||||
if (info->thingClassId() == tempoConnectionThingClassId) {
|
||||
|
||||
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<Tempo::Account> &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<Tempo::Team> &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<Tempo *>(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<Tempo *>(sender());
|
||||
Thing *thing = myThings().findById(m_tempoConnections.key(tempo));
|
||||
if (!thing)
|
||||
return;
|
||||
|
||||
thing->setStateValue(tempoConnectionLoggedInStateTypeId, authenticated);
|
||||
}
|
||||
|
||||
void IntegrationPluginTempo::onAccountsReceived(const QList<Tempo::Account> 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<Tempo::Team> 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<Tempo::Worklog> 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<Tempo::Worklog> 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);
|
||||
}
|
||||
}
|
||||
|
||||
74
tempo/integrationplugintempo.h
Normal file
74
tempo/integrationplugintempo.h
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* For any further details and any questions please contact us under
|
||||
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||
* https://nymea.io/license/faq
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef INTEGRATIONPLUGINTEMPO_H
|
||||
#define INTEGRATIONPLUGINTEMPO_H
|
||||
|
||||
#include "integrations/integrationplugin.h"
|
||||
#include "plugintimer.h"
|
||||
|
||||
#include "tempo.h"
|
||||
|
||||
class IntegrationPluginTempo : public IntegrationPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugintempo.json")
|
||||
Q_INTERFACES(IntegrationPlugin)
|
||||
|
||||
public:
|
||||
explicit IntegrationPluginTempo();
|
||||
|
||||
void startPairing(ThingPairingInfo *info) override;
|
||||
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
|
||||
|
||||
void 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<ThingId, QList<Tempo::Worklog>> m_worklogBuffer;
|
||||
QHash<ThingId, Tempo *> m_setupTempoConnections;
|
||||
QHash<ThingId, Tempo *> m_tempoConnections;
|
||||
|
||||
private slots:
|
||||
void onConnectionChanged(bool connected);
|
||||
void onAuthenticationStatusChanged(bool authenticated);
|
||||
|
||||
void onAccountsReceived(const QList<Tempo::Account> accounts);
|
||||
void onTeamsReceived(const QList<Tempo::Team> teams);
|
||||
|
||||
void onAccountWorkloadReceived(const QString &accountKey, QList<Tempo::Worklog> workloads, int limit, int offset);
|
||||
void onTeamWorkloadReceived(int teamId, QList<Tempo::Worklog> workloads, int limit, int offset);
|
||||
};
|
||||
#endif // INTEGRATIONPLUGINTEMPO_H
|
||||
211
tempo/integrationplugintempo.json
Normal file
211
tempo/integrationplugintempo.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tempo/meta.json
Normal file
13
tempo/meta.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Tempo",
|
||||
"tagline": "Get logged hours from Tempo.",
|
||||
"icon": "tempo.svg",
|
||||
"stability": "consumer",
|
||||
"offline": false,
|
||||
"technologies": [
|
||||
"cloud"
|
||||
],
|
||||
"categories": [
|
||||
"online-service"
|
||||
]
|
||||
}
|
||||
353
tempo/tempo.cpp
Normal file
353
tempo/tempo.cpp
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* For any further details and any questions please contact us under
|
||||
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||
* https://nymea.io/license/faq
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "tempo.h"
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
Tempo::Tempo(NetworkAccessManager *networkmanager, const 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<Team> 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<Account> accounts;
|
||||
Q_FOREACH(QVariant var, accountList) {
|
||||
QVariantMap map = var.toMap();
|
||||
Account account;
|
||||
account.self = map["self"].toString();
|
||||
account.key = map["key"].toString();
|
||||
account.id = map["id"].toInt();
|
||||
account.name = map["name"].toString();
|
||||
|
||||
if (map["status"] == "OPEN") {
|
||||
account.status = Status::Open;
|
||||
} else if (map["status"] == "CLOSED") {
|
||||
account.status = Status::Closed;
|
||||
} else if (map["status"] == "ARCHIVED") {
|
||||
account.status = Status::Archived;
|
||||
}
|
||||
account.global = map["global"].toBool();
|
||||
account.monthlyBudget = map["monthlyBudget"].toInt();
|
||||
|
||||
QVariantMap lead = map["lead"].toMap();
|
||||
account.lead.self = lead["self"].toString();
|
||||
account.lead.accountId = lead["accountId"].toString();
|
||||
account.lead.displayName = lead["displayName"].toString();
|
||||
|
||||
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<Worklog> 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<Worklog> 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::Worklog> 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<Worklog> 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;
|
||||
}
|
||||
151
tempo/tempo.h
Normal file
151
tempo/tempo.h
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* For any further details and any questions please contact us under
|
||||
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||
* https://nymea.io/license/faq
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
#ifndef TEMPO_H
|
||||
#define TEMPO_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QTimer>
|
||||
|
||||
#include "network/networkaccessmanager.h"
|
||||
|
||||
class Tempo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
enum Status {
|
||||
Open,
|
||||
Closed,
|
||||
Archived
|
||||
};
|
||||
Q_ENUM(Status)
|
||||
struct Lead {
|
||||
QUrl self;
|
||||
QString accountId;
|
||||
QString displayName;
|
||||
};
|
||||
|
||||
struct Contact {
|
||||
QUrl self;
|
||||
QString accountId;
|
||||
QString displayName;
|
||||
QString type;
|
||||
};
|
||||
|
||||
struct Category {
|
||||
QUrl self;
|
||||
QString 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<Worklog> 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<Team> teams);
|
||||
void accountsReceived(const QList<Account> accounts);
|
||||
void accountWorklogsReceived(const QString &accountKey, QList<Worklog> worklogs, int limit, int offset);
|
||||
void teamWorklogsReceived(int teamId, QList<Worklog> worklogs, int limit, int offset);
|
||||
};
|
||||
|
||||
#endif // TEMPO_H
|
||||
BIN
tempo/tempo.png
Normal file
BIN
tempo/tempo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
14
tempo/tempo.pro
Normal file
14
tempo/tempo.pro
Normal file
@ -0,0 +1,14 @@
|
||||
include(../plugins.pri)
|
||||
|
||||
QT += network
|
||||
|
||||
TARGET = $$qtLibraryTarget(nymea_integrationplugintempo)
|
||||
|
||||
SOURCES += \
|
||||
integrationplugintempo.cpp \
|
||||
tempo.cpp
|
||||
|
||||
HEADERS += \
|
||||
integrationplugintempo.h \
|
||||
tempo.h
|
||||
|
||||
285
tempo/translations/809bc4ca-d1cd-4279-9e0d-7324537ccb5a-en_US.ts
Normal file
285
tempo/translations/809bc4ca-d1cd-4279-9e0d-7324537ccb5a-en_US.ts
Normal file
@ -0,0 +1,285 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1">
|
||||
<context>
|
||||
<name>IntegrationPluginTempo</name>
|
||||
<message>
|
||||
<location filename="../integrationplugintempo.cpp" line="59"/>
|
||||
<source>Please enter your Tempo API integration token.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../integrationplugintempo.cpp" line="62"/>
|
||||
<source>Tempo server not reachable, please check the internet connection.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../integrationplugintempo.cpp" line="102"/>
|
||||
<source>Create a Tempo connection first</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../integrationplugintempo.cpp" line="184"/>
|
||||
<source>Token is not available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>tempo</name>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="81"/>
|
||||
<source>Account</source>
|
||||
<extracomment>The name of the ThingClass ({8be71352-bdfd-450b-903e-79a4ed203701})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="84"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="87"/>
|
||||
<source>Category</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="90"/>
|
||||
<source>Category changed</source>
|
||||
<extracomment>The name of the EventType ({3af6d1c0-bb0a-406f-809b-2c367e1a16bb}) of ThingClass account</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="93"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="96"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="99"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="102"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="105"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="108"/>
|
||||
<source>Connected</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="111"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="114"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="117"/>
|
||||
<source>Connected changed</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="120"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="123"/>
|
||||
<source>Contact</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="126"/>
|
||||
<source>Contact changed</source>
|
||||
<extracomment>The name of the EventType ({ece43b12-4a0d-4e25-b811-b1aca610bea8}) of ThingClass account</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="129"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="132"/>
|
||||
<source>Customer</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="135"/>
|
||||
<source>Customer changed</source>
|
||||
<extracomment>The name of the EventType ({3dcc1426-51f8-46fa-9967-5a93d7bb2633}) of ThingClass account</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="138"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="141"/>
|
||||
<source>Global</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="144"/>
|
||||
<source>Global changed</source>
|
||||
<extracomment>The name of the EventType ({abd55ea0-ad4e-413e-bc77-3e8b7f0a9be4}) of ThingClass account</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="147"/>
|
||||
<source>Id</source>
|
||||
<extracomment>The name of the ParamType (ThingClass: team, Type: thing, ID: {bb90e986-fcfa-47e8-8783-f2b5a887314a})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="150"/>
|
||||
<source>Key</source>
|
||||
<extracomment>The name of the ParamType (ThingClass: account, Type: thing, ID: {c6aeddae-56af-496d-a419-1635ff9bae50})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="153"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="156"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="159"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="162"/>
|
||||
<source>Lead</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="165"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="168"/>
|
||||
<source>Lead changed</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="171"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="174"/>
|
||||
<source>Logged in</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="177"/>
|
||||
<source>Logged in changed</source>
|
||||
<extracomment>The name of the EventType ({e4b5be87-dbc9-481e-88da-608c71be8bda}) of ThingClass tempoConnection</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="180"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="183"/>
|
||||
<source>Monthly budget</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="186"/>
|
||||
<source>Monthly budget changed</source>
|
||||
<extracomment>The name of the EventType ({44ebbc18-7511-48c0-860b-c4de5f634ed6}) of ThingClass account</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="189"/>
|
||||
<source>Start date</source>
|
||||
<extracomment>The name of the ParamType (ThingClass: account, Type: settings, ID: {56c460b2-37d8-453a-b4f4-8e58be348f85})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="192"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="195"/>
|
||||
<source>Status</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="198"/>
|
||||
<source>Status changed</source>
|
||||
<extracomment>The name of the EventType ({7948f15b-7243-404e-9e67-18e915e8b328}) of ThingClass account</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="201"/>
|
||||
<source>Team</source>
|
||||
<extracomment>The name of the ThingClass ({11c85176-e7fe-44b4-995a-24757273f3af})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="204"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="207"/>
|
||||
<source>Tempo</source>
|
||||
<extracomment>The name of the vendor ({58fc1ab7-b8b5-4e52-8388-72957ce5852d})
|
||||
----------
|
||||
The name of the plugin tempo ({809bc4ca-d1cd-4279-9e0d-7324537ccb5a})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="210"/>
|
||||
<source>Tempo connection</source>
|
||||
<extracomment>The name of the ThingClass ({878eae0a-6217-4b36-bd46-72c911e52e73})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="213"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="216"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="219"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="222"/>
|
||||
<source>This month time spent</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="225"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="228"/>
|
||||
<source>This month time spent changed</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="231"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="234"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="237"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="240"/>
|
||||
<source>Total time spent</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="243"/>
|
||||
<location filename="../../../build-nymea-plugins-Desktop-Debug/tempo/plugininfo.h" line="246"/>
|
||||
<source>Total time spent changed</source>
|
||||
<extracomment>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</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
Loading…
x
Reference in New Issue
Block a user