diff --git a/debian/control b/debian/control index 82db62d1..13dcc03a 100644 --- a/debian/control +++ b/debian/control @@ -159,6 +159,9 @@ Multi-Arch: same Depends: ${shlibs:Depends}, ${misc:Depends}, Replaces: libguh1 +Provides: nymea-update-plugin-api-1, + nymea-zeroconf-plugin-api-1, + nymea-system-plugin-api-1 Description: An open source IoT server - core library The nymea daemon is a plugin based IoT (Internet of Things) server. The server works like a translator for devices, things and services and diff --git a/libnymea-core/devices/devicemanagerimplementation.cpp b/libnymea-core/devices/devicemanagerimplementation.cpp index 3c90abda..b7cc858d 100644 --- a/libnymea-core/devices/devicemanagerimplementation.cpp +++ b/libnymea-core/devices/devicemanagerimplementation.cpp @@ -1040,12 +1040,6 @@ DeviceActionInfo *DeviceManagerImplementation::executeAction(const Action &actio return info; } -/*! Centralized time tick for the NymeaTimer resource. Ticks every second. */ -void DeviceManagerImplementation::timeTick() -{ - -} - void DeviceManagerImplementation::loadPlugins() { foreach (const QString &path, pluginSearchDirs()) { diff --git a/libnymea-core/devices/devicemanagerimplementation.h b/libnymea-core/devices/devicemanagerimplementation.h index b3e7918e..25853ae2 100644 --- a/libnymea-core/devices/devicemanagerimplementation.h +++ b/libnymea-core/devices/devicemanagerimplementation.h @@ -114,9 +114,6 @@ public: signals: void loaded(); -public slots: - void timeTick(); - private slots: void loadPlugins(); void loadPlugin(DevicePlugin *pluginIface, const PluginMetadata &metaData); diff --git a/libnymea-core/jsonrpc/configurationhandler.cpp b/libnymea-core/jsonrpc/configurationhandler.cpp index faf2ebff..e67593d5 100644 --- a/libnymea-core/jsonrpc/configurationhandler.cpp +++ b/libnymea-core/jsonrpc/configurationhandler.cpp @@ -61,6 +61,8 @@ #include "configurationhandler.h" #include "nymeacore.h" #include "nymeaconfiguration.h" +#include "platform/platform.h" +#include "platform/platformsystemcontroller.h" namespace nymeaserver { @@ -80,21 +82,21 @@ ConfigurationHandler::ConfigurationHandler(QObject *parent): QString description; QVariantMap params; QVariantMap returns; description = "Get the list of available timezones."; returns.insert("timeZones", QVariantList() << enumValueName(String)); - registerMethod("GetTimeZones", description, params, returns); + registerMethod("GetTimeZones", description, params, returns, "Use System.GetTimeZones instead."); params.clear(); returns.clear(); - description = "DEPRECATED - Use the locale property in the Handshake message instead - Returns a list of locale codes available for the server. i.e. en_US, de_AT"; + description = "Returns a list of locale codes available for the server. i.e. en_US, de_AT"; returns.insert("languages", QVariantList() << enumValueName(String)); - registerMethod("GetAvailableLanguages", description, params, returns); + registerMethod("GetAvailableLanguages", description, params, returns, "Use the locale property in the Handshake message instead."); params.clear(); returns.clear(); description = "Get all configuration parameters of the server."; QVariantMap basicConfiguration; basicConfiguration.insert("serverName", enumValueName(String)); basicConfiguration.insert("serverUuid", enumValueName(Uuid)); - basicConfiguration.insert("serverTime", enumValueName(Uint)); - basicConfiguration.insert("timeZone", enumValueName(String)); - basicConfiguration.insert("language", enumValueName(String)); + basicConfiguration.insert("d:serverTime", enumValueName(Uint)); + basicConfiguration.insert("d:timeZone", enumValueName(String)); + basicConfiguration.insert("d:language", enumValueName(String)); basicConfiguration.insert("debugServerEnabled", enumValueName(Bool)); returns.insert("basicConfiguration", basicConfiguration); QVariantList tcpServerConfigurations; @@ -123,13 +125,13 @@ ConfigurationHandler::ConfigurationHandler(QObject *parent): description = "Set the time zone of the server. See also: \"GetTimeZones\""; params.insert("timeZone", enumValueName(String)); returns.insert("configurationError", enumRef()); - registerMethod("SetTimeZone", description, params, returns); + registerMethod("SetTimeZone", description, params, returns, "Use System.SetTimeZone instead."); params.clear(); returns.clear(); - description = "DEPRECATED - Use the locale property in the Handshake message instead - Sets the server language to the given language. See also: \"GetAvailableLanguages\""; + description = "Sets the server language to the given language. See also: \"GetAvailableLanguages\""; params.insert("language", enumValueName(String)); returns.insert("configurationError", enumRef()); - registerMethod("SetLanguage", description, params, returns); + registerMethod("SetLanguage", description, params, returns, "Use the locale property in the Handshake message instead."); params.clear(); returns.clear(); description = "Enable or disable the debug server."; @@ -339,7 +341,7 @@ JsonReply *ConfigurationHandler::GetTimeZones(const QVariantMap ¶ms) const { Q_UNUSED(params) QVariantList timeZones; - foreach (const QByteArray &timeZoneId, NymeaCore::instance()->timeManager()->availableTimeZones()) { + foreach (const QByteArray &timeZoneId, QTimeZone::availableTimeZoneIds()) { timeZones.append(QString::fromUtf8(timeZoneId)); } @@ -371,11 +373,18 @@ JsonReply *ConfigurationHandler::SetTimeZone(const QVariantMap ¶ms) const { qCDebug(dcJsonRpc()) << "Setting time zone to" << params.value("timeZone").toString(); - QByteArray timeZone = params.value("timeZone").toString().toUtf8(); - if (!NymeaCore::instance()->timeManager()->setTimeZone(timeZone)) - return createReply(statusToReply(NymeaConfiguration::ConfigurationErrorInvalidTimeZone)); + QByteArray timeZoneName = params.value("timeZone").toString().toUtf8(); + + QTimeZone timeZone(timeZoneName); + if (!timeZone.isValid()) { + return createReply(statusToReply(NymeaConfiguration::ConfigurationErrorInvalidTimeZone)); + } + + bool success = NymeaCore::instance()->platform()->systemController()->setTimeZone(timeZone); + if (!success) { + return createReply(statusToReply(NymeaConfiguration::ConfigurationErrorInvalidTimeZone)); + } - NymeaCore::instance()->configuration()->setTimeZone(timeZone); return createReply(statusToReply(NymeaConfiguration::ConfigurationErrorNoError)); } @@ -658,7 +667,7 @@ QVariantMap ConfigurationHandler::packBasicConfiguration() basicConfiguration.insert("serverName", NymeaCore::instance()->configuration()->serverName()); basicConfiguration.insert("serverUuid", NymeaCore::instance()->configuration()->serverUuid().toString()); basicConfiguration.insert("serverTime", NymeaCore::instance()->timeManager()->currentDateTime().toTime_t()); - basicConfiguration.insert("timeZone", QString::fromUtf8(NymeaCore::instance()->timeManager()->timeZone())); + basicConfiguration.insert("timeZone", QTimeZone::systemTimeZoneId()); basicConfiguration.insert("language", NymeaCore::instance()->configuration()->locale().name()); basicConfiguration.insert("debugServerEnabled", NymeaCore::instance()->configuration()->debugServerEnabled()); return basicConfiguration; diff --git a/libnymea-core/jsonrpc/jsonvalidator.cpp b/libnymea-core/jsonrpc/jsonvalidator.cpp index 9846e834..601ab04c 100644 --- a/libnymea-core/jsonrpc/jsonvalidator.cpp +++ b/libnymea-core/jsonrpc/jsonvalidator.cpp @@ -112,7 +112,7 @@ JsonValidator::Result JsonValidator::validateMap(const QVariantMap &map, const Q continue; } QString trimmedKey = key; - trimmedKey.remove(QRegExp("^(o:|r:)")); + trimmedKey.remove(QRegExp("^(o:|r:|d:)")); if (!map.contains(trimmedKey)) { return Result(false, "Missing required key: " + key, key); } @@ -123,7 +123,7 @@ JsonValidator::Result JsonValidator::validateMap(const QVariantMap &map, const Q // Is the key allowed in here? QVariant expectedValue = definition.value(key); foreach (const QString &definitionKey, definition.keys()) { - QRegExp regExp = QRegExp("(o:|r:)*" + key); + QRegExp regExp = QRegExp("(o:|r:|d:)*" + key); if (regExp.exactMatch(definitionKey)) { expectedValue = definition.value(definitionKey); } @@ -132,7 +132,7 @@ JsonValidator::Result JsonValidator::validateMap(const QVariantMap &map, const Q expectedValue = definition.value("o:" + key); } if (!expectedValue.isValid()) { - expectedValue = definition.value("o:" + key); + expectedValue = definition.value("d:" + key); } if (!expectedValue.isValid()) { return Result(false, "Invalid key: " + key); diff --git a/libnymea-core/jsonrpc/systemhandler.cpp b/libnymea-core/jsonrpc/systemhandler.cpp index c433f97b..eda6312a 100644 --- a/libnymea-core/jsonrpc/systemhandler.cpp +++ b/libnymea-core/jsonrpc/systemhandler.cpp @@ -38,9 +38,14 @@ SystemHandler::SystemHandler(Platform *platform, QObject *parent): // Methods QString description; QVariantMap params; QVariantMap returns; - description = "Get the list of capabilites on this system. This allows reading whether things like rebooting or shutting down the system running nymea:core is supported on this host."; + description = "Get the list of capabilites on this system. The property \"powerManagement\" indicates whether " + "rebooting or shutting down is supported on this system. The property \"updateManagement indicates " + "whether system update features are available in this system. The property \"timeManagement\" " + "indicates whether the system time can be configured on this system. Note that GetTime will be " + "available in any case."; returns.insert("powerManagement", enumValueName(Bool)); returns.insert("updateManagement", enumValueName(Bool)); + returns.insert("timeManagement", enumValueName(Bool)); registerMethod("GetCapabilities", description, params, returns); params.clear(); returns.clear(); @@ -116,6 +121,39 @@ SystemHandler::SystemHandler(Platform *platform, QObject *parent): returns.insert("success", enumValueName(Bool)); registerMethod("EnableRepository", description, params, returns); + params.clear(); returns.clear(); + description = "Get the system time and configuraton. The \"time\" and \"timeZone\" properties " + "give the current server time and time zone. \"automaticTimeAvailable\" indicates whether " + "this system supports automatically setting the clock (e.g. using NTP). \"automaticTime\" will " + "be true if the system is configured to automatically update the clock."; + returns.insert("time", enumValueName(Uint)); + returns.insert("timeZone", enumValueName(String)); + returns.insert("automaticTimeAvailable", enumValueName(Bool)); + returns.insert("automaticTime", enumValueName(Bool)); + registerMethod("GetTime", description, params, returns); + + params.clear(); returns.clear(); + description = "Set the system time configuraton. The system can be configured to update the time automatically " + "by setting \"automaticTime\" to true. This will only work if the \"timeManagement\" capability is " + "available on this system and \"GetTime\" indicates the availability of automatic time settings. If " + "any of those requirements are not met, this method will return \"false\" in the \"success\" property. " + "In order to manually configure the time, \"automaticTime\" should be set to false and \"time\" should " + "be set. Note that if \"automaticTime\" is set to true and a manual \"time\" is still passed, the system " + "will attempt to configure automatic time updates and only set the manual time if automatic mode fails. " + "A time zone can always be passed optionally to change the system time zone and should be a IANA time zone " + "id."; + params.insert("o:automaticTime", enumValueName(Bool)); + params.insert("o:time", enumValueName(Uint)); + params.insert("o:timeZone", enumValueName(String)); + returns.insert("success", enumValueName(Bool)); + registerMethod("SetTime", description, params, returns); + + params.clear(); returns.clear(); + description = "Returns the list of IANA specified time zone IDs which can be used to select a time zone. It is not " + "required to use this method if the client toolkit already provides means to obtain a list of IANA time " + "zone ids."; + returns.insert("timeZones", enumValueName(StringList)); + registerMethod("GetTimeZones", description, params, returns); // Notifications params.clear(); @@ -160,6 +198,13 @@ SystemHandler::SystemHandler(Platform *platform, QObject *parent): params.insert("repositoryId", enumValueName(String)); registerNotification("RepositoryRemoved", description, params); + params.clear(); + description = "Emitted whenever the time configuration is changed"; + params.insert("time", enumValueName(Uint)); + params.insert("timeZone", enumValueName(String)); + params.insert("automaticTimeAvailable", enumValueName(Bool)); + params.insert("automaticTime", enumValueName(Bool)); + registerNotification("TimeConfigurationChanged", description, params); connect(m_platform->systemController(), &PlatformSystemController::availableChanged, this, &SystemHandler::onCapabilitiesChanged); connect(m_platform->updateController(), &PlatformUpdateController::availableChanged, this, &SystemHandler::onCapabilitiesChanged); @@ -205,6 +250,14 @@ SystemHandler::SystemHandler(Platform *platform, QObject *parent): params.insert("repositoryId", repositoryId); emit RepositoryRemoved(params); }); + connect(m_platform->systemController(), &PlatformSystemController::timeConfigurationChanged, this, [this](){ + QVariantMap params; + params.insert("time", QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000); + params.insert("timeZone", QTimeZone::systemTimeZoneId()); + params.insert("automaticTimeAvailable", m_platform->systemController()->automaticTimeAvailable()); + params.insert("automaticTime", m_platform->systemController()->automaticTime()); + emit TimeConfigurationChanged(params); + }, Qt::QueuedConnection); // Queued to give QDateTime a chance to sync itself to the system } QString SystemHandler::name() const @@ -218,6 +271,7 @@ JsonReply *SystemHandler::GetCapabilities(const QVariantMap ¶ms) QVariantMap data; data.insert("powerManagement", m_platform->systemController()->powerManagementAvailable()); data.insert("updateManagement", m_platform->updateController()->updateManagementAvailable()); + data.insert("timeManagement", m_platform->systemController()->timeManagementAvailable()); return createReply(data); } @@ -314,6 +368,62 @@ JsonReply *SystemHandler::EnableRepository(const QVariantMap ¶ms) const return createReply(returns); } +JsonReply *SystemHandler::GetTime(const QVariantMap ¶ms) const +{ + Q_UNUSED(params) + QVariantMap returns; + returns.insert("automaticTimeAvailable", m_platform->systemController()->automaticTimeAvailable()); + returns.insert("automaticTime", m_platform->systemController()->automaticTime()); + returns.insert("time", QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000); + returns.insert("timeZone", QTimeZone::systemTimeZoneId()); + return createReply(returns); +} + +JsonReply *SystemHandler::SetTime(const QVariantMap ¶ms) const +{ + QVariantMap returns; + bool handled = false; + bool automaticTime = params.value("automaticTime", false).toBool(); + if (params.contains("automaticTime") && m_platform->systemController()->automaticTimeAvailable()) { + if (!m_platform->systemController()->setAutomaticTime(automaticTime)) { + returns.insert("success", false); + return createReply(returns); + } + handled = true; + } + if (!automaticTime && params.contains("time")) { + QDateTime time = QDateTime::fromMSecsSinceEpoch(params.value("time").toLongLong() * 1000); + if (!m_platform->systemController()->setTime(time)) { + returns.insert("success", false); + return createReply(returns); + } + handled = true; + } + if (params.contains("timeZone")) { + QTimeZone timeZone(params.value("timeZone").toByteArray()); + if (!m_platform->systemController()->setTimeZone(timeZone)) { + returns.insert("success", false); + return createReply(returns); + } + handled = true; + } + returns.insert("success", handled); + return createReply(returns); +} + +JsonReply *SystemHandler::GetTimeZones(const QVariantMap ¶ms) const +{ + Q_UNUSED(params) + QVariantList timeZones; + foreach (const QByteArray &timeZoneId, QTimeZone::availableTimeZoneIds()) { + timeZones.append(QString::fromUtf8(timeZoneId)); + } + + QVariantMap returns; + returns.insert("timeZones", timeZones); + return createReply(returns); +} + void SystemHandler::onCapabilitiesChanged() { QVariantMap caps; diff --git a/libnymea-core/jsonrpc/systemhandler.h b/libnymea-core/jsonrpc/systemhandler.h index 4c43a4e8..b54f058a 100644 --- a/libnymea-core/jsonrpc/systemhandler.h +++ b/libnymea-core/jsonrpc/systemhandler.h @@ -53,8 +53,13 @@ public: Q_INVOKABLE JsonReply *GetRepositories(const QVariantMap ¶ms) const; Q_INVOKABLE JsonReply *EnableRepository(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *GetTime(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *SetTime(const QVariantMap ¶ms) const; + Q_INVOKABLE JsonReply *GetTimeZones(const QVariantMap ¶ms) const; + signals: void CapabilitiesChanged(const QVariantMap ¶ms); + void UpdateStatusChanged(const QVariantMap ¶ms); void PackageAdded(const QVariantMap ¶ms); void PackageChanged(const QVariantMap ¶ms); @@ -63,6 +68,8 @@ signals: void RepositoryChanged(const QVariantMap ¶ms); void RepositoryRemoved(const QVariantMap ¶ms); + void TimeConfigurationChanged(const QVariantMap ¶ms); + private slots: void onCapabilitiesChanged(); diff --git a/libnymea-core/nymeacore.cpp b/libnymea-core/nymeacore.cpp index b8e0e380..2df324a2 100644 --- a/libnymea-core/nymeacore.cpp +++ b/libnymea-core/nymeacore.cpp @@ -98,6 +98,7 @@ #include "tagging/tagsstorage.h" #include "platform/platform.h" #include "experiences/experiencemanager.h" +#include "platform/platformsystemcontroller.h" #include "scriptengine/scriptengine.h" #include "jsonrpc/scriptshandler.h" @@ -143,7 +144,14 @@ void NymeaCore::init() { m_configuration = new NymeaConfiguration(this); qCDebug(dcApplication()) << "Creating Time Manager"; - m_timeManager = new TimeManager(m_configuration->timeZone(), this); + // Migration path: nymea < 0.18 doesn't use system time zone but stores its own time zone in the config + // For migration, let's set the system's time zone to the config now to upgrade to the system time zone based nymea >= 0.18 + if (QTimeZone(m_configuration->timeZone()).isValid()) { + if (m_platform->systemController()->setTimeZone(QTimeZone(m_configuration->timeZone()))) { + m_configuration->setTimeZone(""); + } + } + m_timeManager = new TimeManager(this); qCDebug(dcApplication) << "Creating Log Engine"; m_logger = new LogEngine(m_configuration->logDBDriver(), m_configuration->logDBName(), m_configuration->logDBHost(), m_configuration->logDBUser(), m_configuration->logDBPassword(), m_configuration->logDBMaxEntries(), this); @@ -206,7 +214,6 @@ void NymeaCore::init() { connect(m_ruleEngine, &RuleEngine::ruleConfigurationChanged, this, &NymeaCore::ruleConfigurationChanged); connect(m_timeManager, &TimeManager::dateTimeChanged, this, &NymeaCore::onDateTimeChanged); - connect(m_timeManager, &TimeManager::tick, m_deviceManager, &DeviceManagerImplementation::timeTick); m_logger->logSystemEvent(m_timeManager->currentDateTime(), true); } diff --git a/libnymea-core/time/timemanager.cpp b/libnymea-core/time/timemanager.cpp index 14876caf..ccc200c5 100644 --- a/libnymea-core/time/timemanager.cpp +++ b/libnymea-core/time/timemanager.cpp @@ -40,71 +40,16 @@ namespace nymeaserver { /*! Constructs a new \l{TimeManager} with the given \a timeZone and \a parent. */ -TimeManager::TimeManager(const QByteArray &timeZone, QObject *parent) : +TimeManager::TimeManager(QObject *parent) : QObject(parent) { - m_dateTime = QDateTime::currentDateTimeUtc(); - m_dateTime.setTimeSpec(Qt::UTC); - - setTimeZone(timeZone); - - m_nymeaTimer = new QTimer(this); - m_nymeaTimer->setInterval(1000); - m_nymeaTimer->setSingleShot(false); - - connect(m_nymeaTimer, &QTimer::timeout, this, &TimeManager::nymeaTimeout); - - m_nymeaTimer->start(); -} - -/*! Returns the time zone of this \l{TimeManager}. */ -QByteArray TimeManager::timeZone() const -{ - return m_timeZone.id(); -} - -/*! Sets the \a timeZone of this \l{TimeManager}. Allowed values according to the \l{http://www.iana.org/time-zones}{IANA database}. - * Returns false if the given timezone is not valid. */ -bool TimeManager::setTimeZone(const QByteArray &timeZone) -{ - if (!QTimeZone(timeZone).isValid()) { - qCWarning(dcTimeManager()) << "Invalid time zone" << timeZone; - qCWarning(dcTimeManager()) << "Using system time zone" << QTimeZone::systemTimeZoneId(); - m_timeZone = QTimeZone(QTimeZone::systemTimeZoneId()); - emit dateTimeChanged(currentDateTime()); - return false; - } - - qCDebug(dcTimeManager()) << "Set time zone" << timeZone; - m_timeZone = QTimeZone(timeZone); - qCDebug(dcTimeManager()) << "UTC" << m_dateTime.toString("dd.MM.yyyy hh:mm:ss"); - qCDebug(dcTimeManager) << "Zone time" << currentDateTime().toString("dd.MM.yyyy hh:mm:ss"); - emit dateTimeChanged(currentDateTime()); - return true; + m_timerId = startTimer(1000, Qt::VeryCoarseTimer); } /*! Returns the current dateTime of this \l{TimeManager}. */ QDateTime TimeManager::currentDateTime() const { - return QDateTime::currentDateTimeUtc().toTimeZone(m_timeZone); -} - -/*! Returns the current time of this \l{TimeManager}. */ -QTime TimeManager::currentTime() const -{ - return QDateTime::currentDateTimeUtc().toTimeZone(m_timeZone).time(); -} - -/*! Returns the current date of this \l{TimeManager}. */ -QDate TimeManager::currentDate() const -{ - return QDateTime::currentDateTimeUtc().toTimeZone(m_timeZone).date(); -} - -/*! Returns a list of available time zones on this system. */ -QList TimeManager::availableTimeZones() const -{ - return QTimeZone::availableTimeZoneIds(); + return QDateTime::currentDateTime().addSecs(m_overrideDifference); } /*! Stop the time. @@ -115,7 +60,7 @@ void TimeManager::stopTimer() { qCWarning(dcTimeManager()) << "TimeManager timer stopped. You should only see this in tests."; // Stop clock (used for testing) - m_nymeaTimer->stop(); + killTimer(m_timerId); } /*! Set the current time of this TimeManager to the given \a dateTime. @@ -125,21 +70,22 @@ void TimeManager::stopTimer() void TimeManager::setTime(const QDateTime &dateTime) { qCWarning(dcTimeManager()) << "TimeManager time changed" << dateTime.toString("dd.MM.yyyy hh:mm:ss") << "You should only see this in tests."; + m_overrideDifference = QDateTime::currentDateTime().secsTo(dateTime); // This method will only be called for testing to set the internal time - emit tick(); emit dateTimeChanged(dateTime); } -void TimeManager::nymeaTimeout() +void TimeManager::timerEvent(QTimerEvent *event) { - // tick for deviceManager + Q_UNUSED(event) + emit tick(); // Minute based nymea time - QDateTime utcDateTime = QDateTime::currentDateTimeUtc(); - if (m_dateTime.time().minute() != utcDateTime.toTimeZone(m_timeZone).time().minute()) { - m_dateTime = utcDateTime; - emit dateTimeChanged(currentDateTime()); + QDateTime now = QDateTime::currentDateTime(); + if (m_lastEvent.time().minute() != now.time().minute()) { + m_lastEvent = now; + emit dateTimeChanged(now.addSecs(m_overrideDifference)); } } diff --git a/libnymea-core/time/timemanager.h b/libnymea-core/time/timemanager.h index 24bd7614..e49d0eb4 100644 --- a/libnymea-core/time/timemanager.h +++ b/libnymea-core/time/timemanager.h @@ -32,32 +32,27 @@ class TimeManager : public QObject { Q_OBJECT public: - explicit TimeManager(const QByteArray &timeZone, QObject *parent = 0); - - QByteArray timeZone() const; - bool setTimeZone(const QByteArray &timeZone = QTimeZone::systemTimeZoneId()); + explicit TimeManager(QObject *parent = nullptr); QDateTime currentDateTime() const; - QTime currentTime() const; - QDate currentDate() const; - - QList availableTimeZones() const; + // For testability only void stopTimer(); void setTime(const QDateTime &dateTime); -private: - QTimeZone m_timeZone; - QDateTime m_dateTime; - QTimer *m_nymeaTimer; - signals: void tick(); void dateTimeChanged(const QDateTime &dateTime); -private slots: - void nymeaTimeout(); +protected: + void timerEvent(QTimerEvent *event) override; +private: + int m_timerId = 0; + QDateTime m_lastEvent; + + // For testability + qint64 m_overrideDifference = 0; }; } diff --git a/libnymea/platform/platformsystemcontroller.cpp b/libnymea/platform/platformsystemcontroller.cpp index dda77859..a2380fe3 100644 --- a/libnymea/platform/platformsystemcontroller.cpp +++ b/libnymea/platform/platformsystemcontroller.cpp @@ -22,6 +22,8 @@ #include "platformsystemcontroller.h" +#include "loggingcategories.h" + PlatformSystemController::PlatformSystemController(QObject *parent) : QObject(parent) { @@ -41,3 +43,39 @@ bool PlatformSystemController::shutdown() { return false; } + +bool PlatformSystemController::timeManagementAvailable() const +{ + return false; +} + +bool PlatformSystemController::automaticTimeAvailable() const +{ + return false; +} + +bool PlatformSystemController::automaticTime() const +{ + return false; +} + +bool PlatformSystemController::setTime(const QDateTime &time) +{ + Q_UNUSED(time) + qCWarning(dcPlatform()) << "setTime not implemented in platform plugin"; + return false; +} + +bool PlatformSystemController::setAutomaticTime(bool automaticTime) +{ + Q_UNUSED(automaticTime) + qCWarning(dcPlatform()) << "setAutomaticTime not implemented in platform plugin"; + return false; +} + +bool PlatformSystemController::setTimeZone(const QTimeZone &timeZone) +{ + Q_UNUSED(timeZone) + qCWarning(dcPlatform()) << "setTimeZone not implemented in platform plugin"; + return false; +} diff --git a/libnymea/platform/platformsystemcontroller.h b/libnymea/platform/platformsystemcontroller.h index a1de5fa9..cced1db9 100644 --- a/libnymea/platform/platformsystemcontroller.h +++ b/libnymea/platform/platformsystemcontroller.h @@ -24,6 +24,7 @@ #define PLATFORMSYSTEMCONTROLLER_H #include +#include class PlatformSystemController : public QObject { @@ -36,8 +37,19 @@ public: virtual bool reboot(); virtual bool shutdown(); + virtual bool timeManagementAvailable() const; + virtual bool automaticTimeAvailable() const; + virtual bool automaticTime() const; + virtual bool setTime(const QDateTime &time); + virtual bool setAutomaticTime(bool automaticTime); + virtual bool setTimeZone(const QTimeZone &timeZone); + + signals: void availableChanged(); + void timeZoneManagementAvailableChanged(); + + void timeConfigurationChanged(); }; Q_DECLARE_INTERFACE(PlatformSystemController, "io.nymea.PlatformSystemController") diff --git a/tests/auto/api.json b/tests/auto/api.json index fb351a2f..1b27568b 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -407,7 +407,8 @@ } }, "Configuration.GetAvailableLanguages": { - "description": "DEPRECATED - Use the locale property in the Handshake message instead - Returns a list of locale codes available for the server. i.e. en_US, de_AT", + "deprecated": "Use the locale property in the Handshake message instead.", + "description": "Returns a list of locale codes available for the server. i.e. en_US, de_AT", "params": { }, "returns": { @@ -422,12 +423,12 @@ }, "returns": { "basicConfiguration": { + "d:language": "String", + "d:serverTime": "Uint", + "d:timeZone": "String", "debugServerEnabled": "Bool", - "language": "String", "serverName": "String", - "serverTime": "Uint", - "serverUuid": "Uuid", - "timeZone": "String" + "serverUuid": "Uuid" }, "cloud": { "enabled": "Bool" @@ -464,6 +465,7 @@ } }, "Configuration.GetTimeZones": { + "deprecated": "Use System.GetTimeZones instead.", "description": "Get the list of available timezones.", "params": { }, @@ -492,7 +494,8 @@ } }, "Configuration.SetLanguage": { - "description": "DEPRECATED - Use the locale property in the Handshake message instead - Sets the server language to the given language. See also: \"GetAvailableLanguages\"", + "deprecated": "Use the locale property in the Handshake message instead.", + "description": "Sets the server language to the given language. See also: \"GetAvailableLanguages\"", "params": { "language": "String" }, @@ -537,6 +540,7 @@ } }, "Configuration.SetTimeZone": { + "deprecated": "Use System.SetTimeZone instead.", "description": "Set the time zone of the server. See also: \"GetTimeZones\"", "params": { "timeZone": "String" @@ -1314,11 +1318,12 @@ } }, "System.GetCapabilities": { - "description": "Get the list of capabilites on this system. This allows reading whether things like rebooting or shutting down the system running nymea:core is supported on this host.", + "description": "Get the list of capabilites on this system. The property \"powerManagement\" indicates whether rebooting or shutting down is supported on this system. The property \"updateManagement indicates whether system update features are available in this system. The property \"timeManagement\" indicates whether the system time can be configured on this system. Note that GetTime will be available in any case.", "params": { }, "returns": { "powerManagement": "Bool", + "timeManagement": "Bool", "updateManagement": "Bool" } }, @@ -1338,6 +1343,25 @@ "repositories": "$ref:Repositories" } }, + "System.GetTime": { + "description": "Get the system time and configuraton. The \"time\" and \"timeZone\" properties give the current server time and time zone. \"automaticTimeAvailable\" indicates whether this system supports automatically setting the clock (e.g. using NTP). \"automaticTime\" will be true if the system is configured to automatically update the clock.", + "params": { + }, + "returns": { + "automaticTime": "Bool", + "automaticTimeAvailable": "Bool", + "time": "Uint", + "timeZone": "String" + } + }, + "System.GetTimeZones": { + "description": "Returns the list of IANA specified time zone IDs which can be used to select a time zone. It is not required to use this method if the client toolkit already provides means to obtain a list of IANA time zone ids.", + "params": { + }, + "returns": { + "timeZones": "StringList" + } + }, "System.GetUpdateStatus": { "description": "Get the current status of the update system. \"busy\" indicates that the system is current busy with an operation regarding updates. This does not necessarily mean an actual update is running. When this is true, update related functions on the client should be marked as busy and no interaction with update components shall be allowed. An example for such a state is when the system queries the server if there are updates available, typically after a call to CheckForUpdates. \"updateRunning\" on the other hand indicates an actual update process is ongoing. The user should be informed about it, the system also might restart at any point while an update is running.", "params": { @@ -1377,6 +1401,17 @@ "success": "Bool" } }, + "System.SetTime": { + "description": "Set the system time configuraton. The system can be configured to update the time automatically by setting \"automaticTime\" to true. This will only work if the \"timeManagement\" capability is available on this system and \"GetTime\" indicates the availability of automatic time settings. If any of those requirements are not met, this method will return \"false\" in the \"success\" property. In order to manually configure the time, \"automaticTime\" should be set to false and \"time\" should be set. Note that if \"automaticTime\" is set to true and a manual \"time\" is still passed, the system will attempt to configure automatic time updates and only set the manual time if automatic mode fails. A time zone can always be passed optionally to change the system time zone and should be a IANA time zone id.", + "params": { + "o:automaticTime": "Bool", + "o:time": "Uint", + "o:timeZone": "String" + }, + "returns": { + "success": "Bool" + } + }, "System.Shutdown": { "description": "Initiate a shutdown of the system. The return value will indicate whether the procedure has been initiated successfully.", "params": { @@ -1433,12 +1468,12 @@ "description": "Emitted whenever the basic configuration of this server changes.", "params": { "basicConfiguration": { + "d:language": "String", + "d:serverTime": "Uint", + "d:timeZone": "String", "debugServerEnabled": "Bool", - "language": "String", "serverName": "String", - "serverTime": "Uint", - "serverUuid": "Uuid", - "timeZone": "String" + "serverUuid": "Uuid" } } }, @@ -1743,6 +1778,15 @@ "repositoryId": "String" } }, + "System.TimeConfigurationChanged": { + "description": "Emitted whenever the time configuration is changed", + "params": { + "automaticTime": "Bool", + "automaticTimeAvailable": "Bool", + "time": "Uint", + "timeZone": "String" + } + }, "System.UpdateStatusChanged": { "description": "Emitted whenever the update status changes.", "params": { diff --git a/tests/auto/configurations/testconfigurations.cpp b/tests/auto/configurations/testconfigurations.cpp index 3bcdb4de..8cf0f552 100644 --- a/tests/auto/configurations/testconfigurations.cpp +++ b/tests/auto/configurations/testconfigurations.cpp @@ -41,7 +41,6 @@ protected slots: private slots: void getConfigurations(); - void testTimeZones(); void testServerName(); void testLanguages(); @@ -81,101 +80,6 @@ void TestConfigurations::getConfigurations() QVERIFY(configurations.contains("webSocketServerConfigurations")); } -void TestConfigurations::testTimeZones() -{ - enableNotifications({"Configuration"}); - - QVariantMap params; QVariant response; QVariantMap configurations; QVariantList configurationChangedNotifications; - - QSignalSpy notificationSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); - - QVariantList timeZones = injectAndWait("Configuration.GetTimeZones").toMap().value("params").toMap().value("timeZones").toList(); - QVERIFY(timeZones.count() > 0); - QVERIFY(timeZones.contains("America/Toronto")); - QVERIFY(timeZones.contains("Europe/Vienna")); - QVERIFY(timeZones.contains("Africa/Dakar")); - - // Get current configurations - QVariantMap basicConfigurationMap = loadBasicConfiguration(); - - // Set timezone unchainged - params.clear(); response.clear(); configurations.clear(); - params.insert("timeZone", basicConfigurationMap.value("timeZone").toString()); - response = injectAndWait("Configuration.SetTimeZone", params); - verifyConfigurationError(response); - - // Check notification not emitted - notificationSpy.wait(200); - configurationChangedNotifications = checkNotifications(notificationSpy, "Configuration.BasicConfigurationChanged"); - QVERIFY2(configurationChangedNotifications.count() == 0, "Got Configuration.BasicConfigurationChanged notification but should have not."); - - // Set new timezone (Africa/Dakar) - QString newTimeZone("Africa/Dakar"); - params.clear(); response.clear(); configurations.clear(); notificationSpy.clear(); - params.insert("timeZone", newTimeZone); - response = injectAndWait("Configuration.SetTimeZone", params); - verifyConfigurationError(response); - - notificationSpy.wait(200); - configurationChangedNotifications = checkNotifications(notificationSpy, "Configuration.BasicConfigurationChanged"); - QVERIFY2(configurationChangedNotifications.count() == 1, "Should get only one Configuration.BasicConfigurationChanged notification"); - QVariantMap notificationContent = configurationChangedNotifications.first().toMap().value("params").toMap(); - - qDebug() << notificationContent; - - QVERIFY2(notificationContent.contains("basicConfiguration"), "Notification does not contain basicConfiguration"); - QVariantMap basicConfigurationNotificationMap = notificationContent.value("basicConfiguration").toMap(); - QVERIFY2(basicConfigurationNotificationMap.contains("language"), "Notification does not contain key language"); - QVERIFY2(basicConfigurationNotificationMap.contains("serverName"), "Notification does not contain key serverName"); - QVERIFY2(basicConfigurationNotificationMap.contains("serverTime"), "Notification does not contain key serverTime"); - QVERIFY2(basicConfigurationNotificationMap.contains("serverUuid"), "Notification does not contain key serverUuid"); - QVERIFY2(basicConfigurationNotificationMap.contains("debugServerEnabled"), "Notification does not contain key debugServerEnabled"); - QVERIFY2(basicConfigurationNotificationMap.contains("timeZone"), "Notification does not contain key timeZone"); - QVERIFY2(basicConfigurationNotificationMap.value("timeZone").toString() == newTimeZone, "Notification does not contain the new timeZone"); - - // Get current timezone and time - params.clear(); response.clear(); configurations.clear(); - configurations = injectAndWait("Configuration.GetConfigurations").toMap().value("params").toMap(); - QString currentTimeZone = configurations.value("basicConfiguration").toMap().value("timeZone").toString(); - int currentTime = configurations.value("basicConfiguration").toMap().value("serverTime").toInt(); - qDebug() << currentTimeZone << QDateTime::fromTime_t(currentTime); - - // Set new timezone - params.clear(); response.clear(); configurations.clear(); - params.insert("timeZone", "Moon/Darkside"); - response = injectAndWait("Configuration.SetTimeZone", params); - verifyConfigurationError(response, NymeaConfiguration::ConfigurationErrorInvalidTimeZone); - - // Set new timezone - params.clear(); response.clear(); configurations.clear(); - params.insert("timeZone", "America/Toronto"); - response = injectAndWait("Configuration.SetTimeZone", params); - verifyConfigurationError(response); - - // Check new timezone - params.clear(); response.clear(); configurations.clear(); - configurations = injectAndWait("Configuration.GetConfigurations").toMap().value("params").toMap(); - newTimeZone = configurations.value("basicConfiguration").toMap().value("timeZone").toString(); - int newTime = configurations.value("basicConfiguration").toMap().value("serverTime").toInt(); - qDebug() << newTimeZone << QDateTime::fromTime_t(newTime); - QVERIFY(currentTimeZone != newTimeZone); - - restartServer(); - - // Check loaded timezone - configurations = injectAndWait("Configuration.GetConfigurations").toMap().value("params").toMap(); - QString reloadedTimeZone = configurations.value("basicConfiguration").toMap().value("timeZone").toString(); - QVERIFY(newTimeZone == reloadedTimeZone); - - // Reset the timezone - params.clear(); response.clear(); - params.insert("timeZone", "Europe/Vienna"); - response = injectAndWait("Configuration.SetTimeZone", params); - verifyConfigurationError(response); - - disableNotifications(); -} - void TestConfigurations::testServerName() { enableNotifications({"Configuration"}); diff --git a/tests/auto/timemanager/testtimemanager.cpp b/tests/auto/timemanager/testtimemanager.cpp index e15336f4..f30886dd 100644 --- a/tests/auto/timemanager/testtimemanager.cpp +++ b/tests/auto/timemanager/testtimemanager.cpp @@ -23,6 +23,9 @@ #include "nymeacore.h" #include "servers/mocktcpserver.h" +#include "platform/platform.h" +#include "platform/platformsystemcontroller.h" + using namespace nymeaserver; class TestTimeManager: public NymeaTestBase @@ -37,9 +40,6 @@ private: private slots: void initTestCase(); - void changeTimeZone_data(); - void changeTimeZone(); - void loadSaveTimeDescriptor_data(); void loadSaveTimeDescriptor(); @@ -142,41 +142,6 @@ void TestTimeManager::initTestCase() "TimeManager.debug=true"); } -void TestTimeManager::changeTimeZone_data() -{ - QTest::addColumn("timeZoneId"); - QTest::addColumn("valid"); - - QTest::newRow("valid timezone: Asia/Tokyo") << QByteArray("Asia/Tokyo") << true; - QTest::newRow("valid timezone: America/Lima") << QByteArray("America/Lima") << true; - QTest::newRow("valid timezone: Africa/Harare") << QByteArray("Africa/Harare") << true; - QTest::newRow("invalid timezone: Mars/Diacria") << QByteArray("Mars/Diacria") << false; - QTest::newRow("invalid timezone: Moon/Kepler") << QByteArray("Moon/Kepler") << false; -} - -void TestTimeManager::changeTimeZone() -{ - QFETCH(QByteArray, timeZoneId); - QFETCH(bool, valid); - - QTimeZone currentTimeZone(NymeaCore::instance()->timeManager()->timeZone()); - QTimeZone newTimeZone(timeZoneId); - - - QDateTime currentDateTime = NymeaCore::instance()->timeManager()->currentDateTime(); - - NymeaCore::instance()->timeManager()->setTimeZone(timeZoneId); - - QDateTime newDateTime = NymeaCore::instance()->timeManager()->currentDateTime(); - - int offsetOriginal = currentTimeZone.offsetFromUtc(currentDateTime); - int offsetNew = newTimeZone.offsetFromUtc(newDateTime); - - if (valid) - QVERIFY(offsetOriginal != offsetNew); - -} - void TestTimeManager::loadSaveTimeDescriptor_data() { // Repeating options @@ -2056,8 +2021,7 @@ void TestTimeManager::initTimeManager() removeAllRules(); enableNotifications({"Rules", "Devices", "Events"}); NymeaCore::instance()->timeManager()->stopTimer(); - qDebug() << NymeaCore::instance()->timeManager()->currentTime().toString(); - qDebug() << NymeaCore::instance()->timeManager()->currentDate().toString(); + qDebug() << NymeaCore::instance()->timeManager()->currentDateTime().toString(); } void TestTimeManager::verifyRuleExecuted(const ActionTypeId &actionTypeId) @@ -2207,7 +2171,9 @@ void TestTimeManager::triggerMockEvent1() QCOMPARE(spy.count(), 1); reply->deleteLater(); - eventSpy.wait(200); + if (eventSpy.isEmpty()) { + eventSpy.wait(); + } QVariantList eventTriggerVariants = checkNotifications(eventSpy, "Events.EventTriggered"); QVERIFY2(eventTriggerVariants.count() == 1, "Did not get Events.EventTriggered notification."); QVERIFY2(eventTriggerVariants.first().toMap().value("params").toMap().contains("event"), "Notification Events.EventTriggered does not contain event.");