From 79841a5e2733995689f1c368533bf8f1ec773951 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sat, 22 Oct 2022 23:04:56 +0200 Subject: [PATCH] Add air conditioning experience --- .../controlviews/devicecontrolapplication.cpp | 4 +- .../airconditioning/airconditioning.pro | 35 + .../airconditioning_plugin.cpp | 23 + .../airconditioning/airconditioning_plugin.h | 15 + .../airconditioningmanager.cpp | 389 +++++++++ .../airconditioning/airconditioningmanager.h | 81 ++ .../libnymea-app-airconditioning.h | 29 + experiences/airconditioning/qmldir | 2 + .../airconditioning/temperatureschedule.cpp | 215 +++++ .../airconditioning/temperatureschedule.h | 92 +++ experiences/airconditioning/zoneinfo.cpp | 216 +++++ experiences/airconditioning/zoneinfo.h | 138 ++++ experiences/experiences.pro | 3 + libnymea-app/connection/nymeahost.h | 1 - libnymea-app/energy/energylogs.cpp | 3 +- libnymea-app/libnymea-app-core.h | 10 +- libnymea-app/libnymea-app.pri | 2 + libnymea-app/models/boolseriesadapter.cpp | 146 ++++ libnymea-app/models/boolseriesadapter.h | 49 ++ libnymea-app/models/logsmodel.cpp | 23 +- libnymea-app/models/logsmodel.h | 1 + libnymea-app/models/logsmodelng.cpp | 61 ++ libnymea-app/models/logsmodelng.h | 1 + libnymea-app/models/xyseriesadapter.cpp | 22 +- libnymea-app/models/xyseriesadapter.h | 2 +- libnymea-app/things.cpp | 3 + libnymea-app/things.h | 3 +- libnymea-app/thingsproxy.cpp | 63 +- libnymea-app/thingsproxy.h | 16 + libnymea-app/zigbee/zigbeeadapter.cpp | 14 +- nymea-app.pro | 6 +- nymea-app/images.qrc | 5 + nymea-app/main.cpp | 6 +- nymea-app/nymea-app.pro | 25 +- nymea-app/resources.qrc | 22 +- nymea-app/ui/MainPage.qml | 8 +- nymea-app/ui/Nymea.qml | 6 +- nymea-app/ui/StyleBase.qml | 7 +- nymea-app/ui/appsettings/CloudLoginPage.qml | 4 +- nymea-app/ui/components/BigThingTile.qml | 2 +- .../ui/components/BrowserContextMenu.qml | 2 +- nymea-app/ui/components/ButtonControls.qml | 1 + nymea-app/ui/components/CircleBackground.qml | 12 +- .../ui/components/ConnectionInfoDialog.qml | 2 +- nymea-app/ui/components/Dial.qml | 87 +- nymea-app/ui/components/ErrorDialog.qml | 2 +- nymea-app/ui/components/Imprint.qml | 2 +- nymea-app/ui/components/MainViewBase.qml | 4 - nymea-app/ui/components/MediaPlayer.qml | 6 +- .../{MeaDialog.qml => NymeaDialog.qml} | 0 nymea-app/ui/components/NymeaSpinBox.qml | 4 +- nymea-app/ui/components/StateDial.qml | 211 +++++ nymea-app/ui/components/ThingContextMenu.qml | 2 +- nymea-app/ui/components/ThingStatusIcons.qml | 2 +- .../ui/connection/CertificateErrorDialog.qml | 2 +- nymea-app/ui/customviews/SensorView.qml | 438 ++++++++++ .../ui/customviews/ThermostatController.qml | 92 ++- nymea-app/ui/delegates/InterfaceTile.qml | 1 + nymea-app/ui/delegates/ParamDelegate.qml | 1 + nymea-app/ui/delegates/SensorListDelegate.qml | 167 ++++ .../devicelistpages/SensorsDeviceListPage.qml | 138 +--- .../ui/devicepages/EvChargerThingPage.qml | 2 +- .../FingerprintReaderDevicePage.qml | 2 +- .../devicepages/NotificationsDevicePage.qml | 2 +- nymea-app/ui/devicepages/SensorDevicePage.qml | 451 +--------- .../ui/devicepages/ThermostatDevicePage.qml | 3 +- .../ui/devicepages/VentilationThingPage.qml | 122 ++- nymea-app/ui/images/calendar.svg | 213 +++++ nymea-app/ui/images/edit-paste.svg | 194 +++++ nymea-app/ui/images/infinity.svg | 175 ++++ nymea-app/ui/images/sensors/window-closed.svg | 82 ++ nymea-app/ui/images/sensors/window-open.svg | 100 +++ nymea-app/ui/magic/EditRulePage.qml | 2 +- nymea-app/ui/magic/ScriptEditor.qml | 4 +- .../ui/mainviews/AirConditioningView.qml | 128 +++ .../airconditioning/ACChartsPage.qml | 777 ++++++++++++++++++ .../airconditioning/ACChartsPage2.qml | 777 ++++++++++++++++++ .../airconditioning/ACSettingsPage.qml | 73 ++ .../airconditioning/BigZoneStatusIcons.qml | 162 ++++ .../airconditioning/CreateZoneWizard.qml | 143 ++++ .../airconditioning/EditZonePage.qml | 153 ++++ .../airconditioning/EditZoneThingsPage.qml | 261 ++++++ .../airconditioning/LegendDelegate.qml | 42 + .../airconditioning/SelectSensorsPage.qml | 73 ++ .../TemperatureScheduleEditor.qml | 406 +++++++++ .../airconditioning/TimeOverrideDialog.qml | 130 +++ .../airconditioning/TimeSchedulePage.qml | 263 ++++++ .../airconditioning/TooltipDelegate.qml | 44 + .../airconditioning/ZoneInfoField.qml | 31 + .../airconditioning/ZoneInfoWrapper.qml | 206 +++++ .../ui/mainviews/airconditioning/ZonePage.qml | 38 + .../airconditioning/ZoneStatusIcons.qml | 67 ++ .../ui/mainviews/airconditioning/ZoneView.qml | 182 ++++ .../mainviews/airconditioning/ZonesView.qml | 127 +++ .../ui/mainviews/dashboard/Dashboard.qml | 2 +- .../dashboard/DashboardAddWizard.qml | 2 +- .../dashboard/DashboardFolderDelegate.qml | 2 +- .../dashboard/DashboardWebViewDelegate.qml | 2 +- .../mainviews/energy/PowerBalanceHistory.qml | 5 +- nymea-app/ui/system/GeneralSettingsPage.qml | 8 +- nymea-app/ui/system/ModbusRtuSettingsPage.qml | 2 +- nymea-app/ui/system/NetworkSettingsPage.qml | 4 +- nymea-app/ui/system/PackageDetailsPage.qml | 4 +- nymea-app/ui/system/SystemUpdatePage.qml | 4 +- nymea-app/ui/system/UsersSettingsPage.qml | 4 +- .../ui/system/zigbee/ZigbeeNetworkPage.qml | 6 +- .../zigbee/ZigbeeNetworkSettingsPage.qml | 4 +- .../zigbee/ZigbeeNetworkTopologyPage.qml | 2 +- nymea-app/ui/system/zigbee/ZigbeeNodePage.qml | 6 +- .../ui/system/zwave/ZWaveNetworkPage.qml | 8 +- .../system/zwave/ZWaveNetworkSettingsPage.qml | 4 +- .../thingconfiguration/ConfigureThingPage.qml | 2 +- .../ui/thingconfiguration/NewThingPage.qml | 255 +++--- nymea-app/ui/utils/AirQualityIndex.qml | 11 + 114 files changed, 7770 insertions(+), 931 deletions(-) create mode 100644 experiences/airconditioning/airconditioning.pro create mode 100644 experiences/airconditioning/airconditioning_plugin.cpp create mode 100644 experiences/airconditioning/airconditioning_plugin.h create mode 100644 experiences/airconditioning/airconditioningmanager.cpp create mode 100644 experiences/airconditioning/airconditioningmanager.h create mode 100644 experiences/airconditioning/libnymea-app-airconditioning.h create mode 100644 experiences/airconditioning/qmldir create mode 100644 experiences/airconditioning/temperatureschedule.cpp create mode 100644 experiences/airconditioning/temperatureschedule.h create mode 100644 experiences/airconditioning/zoneinfo.cpp create mode 100644 experiences/airconditioning/zoneinfo.h create mode 100644 experiences/experiences.pro create mode 100644 libnymea-app/models/boolseriesadapter.cpp create mode 100644 libnymea-app/models/boolseriesadapter.h rename nymea-app/ui/components/{MeaDialog.qml => NymeaDialog.qml} (100%) create mode 100644 nymea-app/ui/components/StateDial.qml create mode 100644 nymea-app/ui/customviews/SensorView.qml create mode 100644 nymea-app/ui/delegates/SensorListDelegate.qml create mode 100644 nymea-app/ui/images/calendar.svg create mode 100644 nymea-app/ui/images/edit-paste.svg create mode 100644 nymea-app/ui/images/infinity.svg create mode 100644 nymea-app/ui/images/sensors/window-closed.svg create mode 100644 nymea-app/ui/images/sensors/window-open.svg create mode 100644 nymea-app/ui/mainviews/AirConditioningView.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ACChartsPage2.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ACSettingsPage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/BigZoneStatusIcons.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/CreateZoneWizard.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/EditZonePage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/EditZoneThingsPage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/LegendDelegate.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/SelectSensorsPage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/TemperatureScheduleEditor.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/TimeOverrideDialog.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/TimeSchedulePage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ZoneInfoField.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ZoneInfoWrapper.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ZonePage.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ZoneStatusIcons.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ZoneView.qml create mode 100644 nymea-app/ui/mainviews/airconditioning/ZonesView.qml diff --git a/androidservice/controlviews/devicecontrolapplication.cpp b/androidservice/controlviews/devicecontrolapplication.cpp index 14be6b03..67d16dfd 100644 --- a/androidservice/controlviews/devicecontrolapplication.cpp +++ b/androidservice/controlviews/devicecontrolapplication.cpp @@ -37,7 +37,9 @@ DeviceControlApplication::DeviceControlApplication(int argc, char *argv[]) : QAp m_engine = new Engine(this); m_qmlEngine = new QQmlApplicationEngine(this); - registerQmlTypes(); + + Nymea::Core::registerQmlTypes(); + qmlRegisterSingletonType("Nymea", 1, 0, "PlatformHelper", platformHelperProvider); qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" ); qmlRegisterType("Nymea", 1, 0, "NfcThingActionWriter"); diff --git a/experiences/airconditioning/airconditioning.pro b/experiences/airconditioning/airconditioning.pro new file mode 100644 index 00000000..9ca7c8ab --- /dev/null +++ b/experiences/airconditioning/airconditioning.pro @@ -0,0 +1,35 @@ +TEMPLATE = lib +CONFIG += staticlib +TARGET = nymea-app-airconditioning + +QT -= gui +QT += network websockets bluetooth charts quick + +include(../../shared.pri) + +LIBS += -L$${top_builddir}/libnymea-app/ -lnymea-app + +android: { +LIBS += -L$${top_builddir}/libnymea-app/$${ANDROID_TARGET_ARCH} +PRE_TARGETDEPS += $$top_builddir/libnymea-app/$${ANDROID_TARGET_ARCH}/libnymea-app.a +} + +INCLUDEPATH += $${top_srcdir}/libnymea-app/ + +# Input +SOURCES += \ + airconditioningmanager.cpp \ + zoneinfo.cpp \ + temperatureschedule.cpp \ + +HEADERS += \ + airconditioningmanager.h \ + libnymea-app-airconditioning.h \ + zoneinfo.h \ + temperatureschedule.h \ + +DISTFILES = + +android: { + DESTDIR = $${ANDROID_TARGET_ARCH} +} diff --git a/experiences/airconditioning/airconditioning_plugin.cpp b/experiences/airconditioning/airconditioning_plugin.cpp new file mode 100644 index 00000000..a0398e48 --- /dev/null +++ b/experiences/airconditioning/airconditioning_plugin.cpp @@ -0,0 +1,23 @@ +#include "airconditioning_plugin.h" + +#include "airconditioningmanager.h" +#include "airconditioningmanager.h" +#include "zoneinfo.h" + +#include +#include + +void AirconditioningPlugin::registerTypes(const char *uri) +{ + + qCritical() << "################# loading plugin"; + // @uri Nymea.AirConditioning + qmlRegisterType(uri, 1, 0, "AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "ZoneInfos", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "ZoneInfo", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TemperatureSchedule", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TemperatureDaySchedule", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TemperatureWeekSchedule", "Get it from AirConditioningManager"); + +} + diff --git a/experiences/airconditioning/airconditioning_plugin.h b/experiences/airconditioning/airconditioning_plugin.h new file mode 100644 index 00000000..1ee8ba1f --- /dev/null +++ b/experiences/airconditioning/airconditioning_plugin.h @@ -0,0 +1,15 @@ +#ifndef AIRCONDITIONING_PLUGIN_H +#define AIRCONDITIONING_PLUGIN_H + +#include + +class AirconditioningPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + void registerTypes(const char *uri) override; +}; + +#endif // AIRCONDITIONING_PLUGIN_H diff --git a/experiences/airconditioning/airconditioningmanager.cpp b/experiences/airconditioning/airconditioningmanager.cpp new file mode 100644 index 00000000..d8fb87f4 --- /dev/null +++ b/experiences/airconditioning/airconditioningmanager.cpp @@ -0,0 +1,389 @@ +#include "airconditioningmanager.h" +#include "zoneinfo.h" + +#include "engine.h" + +#include +#include + +#include "logging.h" + +NYMEA_LOGGING_CATEGORY(dcAirConditioningExperience, "AirConditioningExperience") + +AirConditioningManager::AirConditioningManager(QObject *parent) + : QObject{parent}, + m_zoneInfos(new ZoneInfos(this)) +{ + qRegisterMetaType(); +} + +AirConditioningManager::~AirConditioningManager() +{ + if (m_engine) { + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } +} + +Engine *AirConditioningManager::engine() const +{ + return m_engine; +} + +void AirConditioningManager::setEngine(Engine *engine) +{ + if (m_engine != engine) { + if (m_engine) { + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } + + m_engine = engine; + emit engineChanged(); + + if (m_engine) { + connect(engine, &Engine::destroyed, this, [engine, this]{ if (m_engine == engine) m_engine = nullptr; }); + m_engine->jsonRpcClient()->registerNotificationHandler(this, "AirConditioning", "notificationReceived"); + m_engine->jsonRpcClient()->sendCommand("AirConditioning.GetZones", QVariantMap(), this, "getZonesResponse"); + } + } +} + +ZoneInfos *AirConditioningManager::zoneInfos() const +{ + return m_zoneInfos; +} + +int AirConditioningManager::addZone(const QString &name, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors) +{ + QVariantList thermostatIds, windowSensorIds, indoorSensorIds, outdoorSensorIds; + foreach (const QUuid &id, thermostats) { + thermostatIds.append(id); + } + foreach (const QUuid &id, windowSensors) { + windowSensorIds.append(id); + } + foreach (const QUuid &id, indoorSensors) { + indoorSensorIds.append(id); + } + foreach (const QUuid &id, outdoorSensors) { + outdoorSensorIds.append(id); + } + QVariantMap params = { + {"name", name}, + {"thermostats", thermostatIds}, + {"windowSensors", windowSensorIds}, + {"indoorSensors", indoorSensorIds}, + {"outdoorSensors", outdoorSensorIds} + }; + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.AddZone", params, this, "addZoneResponse"); +} + +int AirConditioningManager::removeZone(const QUuid &zoneId) +{ + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.RemoveZone", {{"zoneId", zoneId}}, this, "removeZoneResponse"); +} + +int AirConditioningManager::setZoneName(const QUuid &zoneId, const QString &name) +{ + QVariantMap params = { + {"zoneId", zoneId}, + {"name", name} + }; + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.SetZoneName", params, this, "setZoneNameResponse"); +} + +int AirConditioningManager::setZoneStandbySetpoint(const QUuid &zoneId, double standbySetpoint) +{ + QVariantMap params = { + {"zoneId", zoneId}, + {"standbySetpoint", standbySetpoint} + }; + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.SetZoneStandbySetpoint", params, this, "setZoneStandbySetpointResponse"); +} + +int AirConditioningManager::setZoneSetpointOverride(const QUuid &zoneId, double setpointOverride, ZoneInfo::SetpointOverrideMode mode, uint minutes) +{ + QMetaEnum modeEnum = QMetaEnum::fromType(); + QVariantMap params = { + {"zoneId", zoneId}, + {"setpointOverride", setpointOverride}, + {"mode", modeEnum.valueToKey(mode)}, + {"minutes", minutes} + }; + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.SetZoneSetpointOverride", params, this, "setZoneSetpointOverrideResponse"); +} + +int AirConditioningManager::setZoneWeekSchedule(const QUuid &zoneId, TemperatureWeekSchedule *weekSchedule) +{ + QVariantList weekList; + for (int day = 0; day < 7; day++) { + TemperatureDaySchedule *daySchedule = weekSchedule->get(day); + QVariantList dayList; + for (int i = 0; i < daySchedule->rowCount(); i++) { + TemperatureSchedule *schedule = daySchedule->get(i); + QVariantMap v = { + {"startTime", schedule->startTime().toString("hh:mm")}, + {"endTime", schedule->endTime().toString("hh:mm")}, + {"temperature", schedule->temperature()} + }; + dayList.append(v); + } + weekList.append(QVariant::fromValue(dayList)); + } + QVariantMap params = { + {"zoneId", zoneId}, + {"weekSchedule", weekList} + }; + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.SetZoneWeekSchedule", params, this, "setZoneWeekScheduleResponse"); +} + +int AirConditioningManager::setZoneThings(const QUuid &zoneId, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors) +{ + QVariantList thermostatIds, windowSensorIds, indoorSensorIds, outdoorSensorIds; + foreach (const QUuid &thingId, thermostats) { + thermostatIds.append(thingId); + } + foreach (const QUuid &thingId, windowSensors) { + windowSensorIds.append(thingId); + } + foreach (const QUuid &thingId, indoorSensors) { + indoorSensorIds.append(thingId); + } + foreach (const QUuid &thingId, outdoorSensors) { + outdoorSensorIds.append(thingId); + } + QVariantMap params = { + {"zoneId", zoneId}, + {"thermostats", thermostatIds}, + {"windowSensors", windowSensorIds}, + {"indoorSensors", indoorSensorIds}, + {"outdoorSensors", outdoorSensorIds}, + }; + return m_engine->jsonRpcClient()->sendCommand("AirConditioning.SetZoneThings", params, this, "setZoneThingsResponse"); +} + +int AirConditioningManager::addZoneThermostat(const QUuid &zoneId, const QUuid &thermostat) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + return setZoneThings(zoneId, zoneInfo->thermostats() << thermostat, zoneInfo->windowSensors(), zoneInfo->indoorSensors(), zoneInfo->outdoorSensors()); +} + +int AirConditioningManager::removeZoneThermostat(const QUuid &zoneId, const QUuid &thermostat) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + QList thermostats = zoneInfo->thermostats(); + thermostats.removeAll(thermostat); + return setZoneThings(zoneId, thermostats, zoneInfo->windowSensors(), zoneInfo->indoorSensors(), zoneInfo->outdoorSensors()); +} + +int AirConditioningManager::addZoneWindowSensor(const QUuid &zoneId, const QUuid &windowSensor) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + return setZoneThings(zoneId, zoneInfo->thermostats(), zoneInfo->windowSensors() << windowSensor, zoneInfo->indoorSensors(), zoneInfo->outdoorSensors()); +} + +int AirConditioningManager::removeWindowSensor(const QUuid &zoneId, const QUuid &windowSensor) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + QList windowSensors = zoneInfo->windowSensors(); + windowSensors.removeAll(windowSensor); + return setZoneThings(zoneId, zoneInfo->thermostats(), windowSensors, zoneInfo->indoorSensors(), zoneInfo->outdoorSensors()); +} + +int AirConditioningManager::addZoneIndoorSensor(const QUuid &zoneId, const QUuid &indoorSensor) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + return setZoneThings(zoneId, zoneInfo->thermostats(), zoneInfo->windowSensors(), zoneInfo->indoorSensors() << indoorSensor, zoneInfo->outdoorSensors()); +} + +int AirConditioningManager::removeZoneIndoorSensor(const QUuid &zoneId, const QUuid &indoorSensor) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + QList indoorSensors = zoneInfo->indoorSensors(); + indoorSensors.removeAll(indoorSensor); + return setZoneThings(zoneId, zoneInfo->thermostats(), zoneInfo->windowSensors(), indoorSensors, zoneInfo->outdoorSensors()); + +} + +int AirConditioningManager::addZoneOutdoorSensor(const QUuid &zoneId, const QUuid &outdoorSensor) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + return setZoneThings(zoneId, zoneInfo->thermostats(), zoneInfo->windowSensors(), zoneInfo->indoorSensors(), zoneInfo->outdoorSensors() << outdoorSensor); +} + +int AirConditioningManager::removeZoneOutdoorSensor(const QUuid &zoneId, const QUuid &outdoorSensor) +{ + ZoneInfo *zoneInfo = m_zoneInfos->getZoneInfo(zoneId); + if (!zoneInfo) { + return -1; + } + QList outdoorSensors = zoneInfo->outdoorSensors(); + outdoorSensors.removeAll(outdoorSensor); + return setZoneThings(zoneId, zoneInfo->thermostats(), zoneInfo->windowSensors(), zoneInfo->indoorSensors(), outdoorSensors); +} + +void AirConditioningManager::notificationReceived(const QVariantMap &data) +{ + QString notification = data.value("notification").toString(); + QVariantMap params = data.value("params").toMap(); + if (notification == "AirConditioning.ZoneAdded") { + QVariantMap zoneMap = params.value("zone").toMap(); + m_zoneInfos->addZoneInfo(unpack(zoneMap)); + + } else if (notification == "AirConditioning.ZoneRemoved") { + QUuid zoneId = params.value("zoneId").toUuid(); + m_zoneInfos->removeZoneInfo(zoneId); + + } else if (notification == "AirConditioning.ZoneChanged") { + QVariantMap zoneMap = params.value("zone").toMap(); + qCDebug(dcAirConditioningExperience()) << "Zone changed:" << qUtf8Printable(QJsonDocument::fromVariant(zoneMap).toJson()); + QUuid zoneId = zoneMap.value("id").toUuid(); + ZoneInfo *zone = m_zoneInfos->getZoneInfo(zoneId); + if (!zone) { + qCWarning(dcAirConditioningExperience()) << "Received a zone changed notification for a zone we don't know" << zoneId; + return; + } + unpack(zoneMap, zone); + } else { + qCDebug(dcAirConditioningExperience()) << "Unhandled notification received" << data; + } +} + +void AirConditioningManager::addZoneResponse(int commandId, const QVariantMap ¶ms) +{ + Q_UNUSED(commandId) + qCDebug(dcAirConditioningExperience()) << "Add zone response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit addZoneReply(commandId, error, params.value("zone").toMap().value("id").toUuid()); +} + +void AirConditioningManager::removeZoneResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcAirConditioningExperience()) << "remove zone response" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit removeZoneReply(commandId, error); +} + +void AirConditioningManager::getZonesResponse(int commandId, const QVariantMap ¶ms) +{ + Q_UNUSED(commandId) + qCDebug(dcAirConditioningExperience()) << "get zones response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); + + foreach (const QVariant &zoneVariant, params.value("zones").toList()) { + m_zoneInfos->addZoneInfo(unpack(zoneVariant.toMap())); + } +} + +void AirConditioningManager::setZoneNameResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcAirConditioningExperience()) << "set zone name response" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit setZoneNameReply(commandId, error); +} + +void AirConditioningManager::setZoneStandbySetpointResponse(int commandId, const QVariantMap ¶ms) +{ + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit setZoneStandbySetpointReply(commandId, error); +} + +void AirConditioningManager::setZoneSetpointOverrideResponse(int commandId, const QVariantMap ¶ms) +{ + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit setZoneSetpointOverrideReply(commandId, error); + +} + +void AirConditioningManager::setZoneWeekScheduleResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcAirConditioningExperience()) << "set zone week schedule response" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit setZoneWeekScheduleReply(commandId, error); + +} + +void AirConditioningManager::setZoneThingsResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcAirConditioningExperience()) << "set zone things response" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + AirConditioningError error = static_cast(metaEnum.keyToValue(params.value("error").toByteArray().data())); + emit setZoneThingsReply(commandId, error); +} + +ZoneInfo *AirConditioningManager::unpack(const QVariantMap &zoneMap, ZoneInfo *zone) +{ + QUuid id = zoneMap.value("id").toUuid(); + + if (!zone) { + zone = new ZoneInfo(id); + } + zone->setName(zoneMap.value("name").toString()); + + QMetaEnum zoneStatusEnum = QMetaEnum::fromType(); + ZoneInfo::ZoneStatus zoneStatus = ZoneInfo::ZoneStatusFlagNone; + foreach (const QVariant &flag, zoneMap.value("zoneStatus").toList()) { + zoneStatus.setFlag(static_cast(zoneStatusEnum.keyToValue(flag.toByteArray())), true); + } + qCDebug(dcAirConditioningExperience()) << "Zone status:" << zoneStatus; + zone->setZoneStatus(zoneStatus); + zone->setCurrentSetpoint(zoneMap.value("currentSetpoint").toDouble()); + zone->setStandbySetpoint(zoneMap.value("standbySetpoint").toDouble()); + QMetaEnum modeEnum = QMetaEnum::fromType(); + ZoneInfo::SetpointOverrideMode mode = static_cast(modeEnum.keyToValue(zoneMap.value("setpointOverrideMode").toByteArray())); + QDateTime end = QDateTime::fromSecsSinceEpoch(zoneMap.value("setpointOverrideEnd").toULongLong()); + zone->setSetpointOverride(zoneMap.value("setpointOverride").toDouble(), mode, end); + + QVariantList weekScheduleList = zoneMap.value("weekSchedule").toList(); + for (int day = 0; day < qMin(7, weekScheduleList.count()); day++) { + QVariant dayVariant = weekScheduleList.at(day); + zone->weekSchedule()->get(day)->clear(); + foreach (const QVariant &scheduleVariant, dayVariant.toList()) { + QVariantMap scheduleMap = scheduleVariant.toMap(); + zone->weekSchedule()->get(day)->createSchedule(scheduleMap.value("startTime").toTime(), scheduleMap.value("endTime").toTime(), scheduleMap.value("temperature").toDouble()); + } + } + QList thermostats, windowSensors, indoorSensors, outdoorSensors; + foreach (const QVariant &variant, zoneMap.value("thermostats").toList()) { + thermostats.append(variant.toUuid()); + } + foreach (const QVariant &variant, zoneMap.value("windowSensors").toList()) { + windowSensors.append(variant.toUuid()); + } + foreach (const QVariant &variant, zoneMap.value("indoorSensors").toList()) { + indoorSensors.append(variant.toUuid()); + } + foreach (const QVariant &variant, zoneMap.value("outdoorSensors").toList()) { + outdoorSensors.append(variant.toUuid()); + } + zone->setThermostats(thermostats); + zone->setWindowSensors(windowSensors); + zone->setIndoorSensors(indoorSensors); + zone->setOutdoorSensors(outdoorSensors); + return zone; +} diff --git a/experiences/airconditioning/airconditioningmanager.h b/experiences/airconditioning/airconditioningmanager.h new file mode 100644 index 00000000..a6d67fe2 --- /dev/null +++ b/experiences/airconditioning/airconditioningmanager.h @@ -0,0 +1,81 @@ +#ifndef AIRCONDITIONINGMANAGER_H +#define AIRCONDITIONINGMANAGER_H + +#include + +#include "zoneinfo.h" + +class Engine; + +class AirConditioningManager : public QObject +{ + Q_OBJECT + Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) + Q_PROPERTY(ZoneInfos* zoneInfos READ zoneInfos CONSTANT) + +public: + enum AirConditioningError { + AirConditioningErrorNoError, + AirConditioningErrorZoneNotFound, + AirConditioningErrorInvalidTimeSpec, + AirConditioningErrorThingNotFound, + AirConditioningErrorInvalidThingType + }; + Q_ENUM(AirConditioningError) + + explicit AirConditioningManager(QObject *parent = nullptr); + ~AirConditioningManager(); + + Engine* engine() const; + void setEngine(Engine *engine); + + ZoneInfos *zoneInfos() const; + + Q_INVOKABLE int addZone(const QString &name, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors); + Q_INVOKABLE int removeZone(const QUuid &zoneId); + Q_INVOKABLE int setZoneName(const QUuid &zoneId, const QString &name); + Q_INVOKABLE int setZoneStandbySetpoint(const QUuid &zoneId, double standbySetpoint); + Q_INVOKABLE int setZoneSetpointOverride(const QUuid &zoneId, double setpointOverride, ZoneInfo::SetpointOverrideMode mode, uint minutes); + Q_INVOKABLE int setZoneWeekSchedule(const QUuid &zoneId, TemperatureWeekSchedule *weekSchedule); + Q_INVOKABLE int setZoneThings(const QUuid &zoneId, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors); + + Q_INVOKABLE int addZoneThermostat(const QUuid &zoneId, const QUuid &thermostat); + Q_INVOKABLE int removeZoneThermostat(const QUuid &zoneId, const QUuid &thermostat); + Q_INVOKABLE int addZoneWindowSensor(const QUuid &zoneId, const QUuid &windowSensor); + Q_INVOKABLE int removeWindowSensor(const QUuid &zoneId, const QUuid &windowSensor); + Q_INVOKABLE int addZoneIndoorSensor(const QUuid &zoneId, const QUuid &indoorSensor); + Q_INVOKABLE int removeZoneIndoorSensor(const QUuid &zoneId, const QUuid &indoorSensor); + Q_INVOKABLE int addZoneOutdoorSensor(const QUuid &zoneId, const QUuid &outdoorSensor); + Q_INVOKABLE int removeZoneOutdoorSensor(const QUuid &zoneId, const QUuid &outdoorSensor); + +signals: + void engineChanged(); + void addZoneReply(int commandId, AirConditioningError error, const QUuid &zoneId); + void removeZoneReply(int commandId, AirConditioningError error); + void setZoneNameReply(int commandId, AirConditioningError error); + void setZoneStandbySetpointReply(int commandId, AirConditioningError error); + void setZoneSetpointOverrideReply(int commandId, AirConditioningError error); + void setZoneThingsReply(int commandId, AirConditioningError error); + void setZoneWeekScheduleReply(int commandId, AirConditioningError error); + +private slots: + void notificationReceived(const QVariantMap &data); + + void addZoneResponse(int commandId, const QVariantMap ¶ms); + void removeZoneResponse(int commandId, const QVariantMap ¶ms); + void getZonesResponse(int commandId, const QVariantMap ¶ms); + void setZoneNameResponse(int commandId, const QVariantMap ¶ms); + void setZoneStandbySetpointResponse(int commandId, const QVariantMap ¶ms); + void setZoneSetpointOverrideResponse(int commandId, const QVariantMap ¶ms); + void setZoneWeekScheduleResponse(int commandId, const QVariantMap ¶ms); + void setZoneThingsResponse(int commandId, const QVariantMap ¶ms); + +private: + ZoneInfo *unpack(const QVariantMap &zoneMap, ZoneInfo *zone = nullptr); + +private: + Engine *m_engine = nullptr; + ZoneInfos *m_zoneInfos = nullptr; +}; + +#endif // AIRCONDITIONINGMANAGER_H diff --git a/experiences/airconditioning/libnymea-app-airconditioning.h b/experiences/airconditioning/libnymea-app-airconditioning.h new file mode 100644 index 00000000..e801131b --- /dev/null +++ b/experiences/airconditioning/libnymea-app-airconditioning.h @@ -0,0 +1,29 @@ +#ifndef LIBNYMEAAPPAIRCONDITIONING_H +#define LIBNYMEAAPPAIRCONDITIONING_H + +#include "airconditioningmanager.h" +#include "zoneinfo.h" + +#include + +namespace Nymea +{ +namespace AirConditioning +{ + +void registerQmlTypes() { + + const char uri[] = "Nymea.AirConditioning"; + // @uri Nymea.AirConditioning + qmlRegisterType(uri, 1, 0, "AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "ZoneInfos", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "ZoneInfo", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TemperatureSchedule", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TemperatureDaySchedule", "Get it from AirConditioningManager"); + qmlRegisterUncreatableType(uri, 1, 0, "TemperatureWeekSchedule", "Get it from AirConditioningManager"); + +} + +} +} +#endif // LIBNYMEAAPPAIRCONDITIONING_H diff --git a/experiences/airconditioning/qmldir b/experiences/airconditioning/qmldir new file mode 100644 index 00000000..2fcf09da --- /dev/null +++ b/experiences/airconditioning/qmldir @@ -0,0 +1,2 @@ +module Nymea.AirConditioning +plugin AirConditioning diff --git a/experiences/airconditioning/temperatureschedule.cpp b/experiences/airconditioning/temperatureschedule.cpp new file mode 100644 index 00000000..2a5f68c6 --- /dev/null +++ b/experiences/airconditioning/temperatureschedule.cpp @@ -0,0 +1,215 @@ +#include "temperatureschedule.h" + +#include + +TemperatureSchedule::TemperatureSchedule(QObject *parent) + : QObject{parent} +{ +// qWarning() << "++++ TempSchedule" << this; + +} + +TemperatureSchedule::~TemperatureSchedule() +{ +// qWarning() << "---- TempSchedule" << this; +} + +QTime TemperatureSchedule::startTime() const +{ + return m_startTime; +} + +void TemperatureSchedule::setStartTime(const QTime &startTime) +{ + if (m_startTime != startTime) { + m_startTime = startTime; + emit startTimeChanged(); + } +} + +QTime TemperatureSchedule::endTime() const +{ + return m_endTime; +} + +void TemperatureSchedule::setEndTime(const QTime &endTime) +{ + if (m_endTime != endTime) { + m_endTime = endTime; + emit endTimeChanged(); + } +} + +double TemperatureSchedule::temperature() const +{ + return m_temperature; +} + +void TemperatureSchedule::setTemperature(double temperature) +{ + if (m_temperature != temperature) { + m_temperature = temperature; + emit temperatureChanged(); + } +} + +TemperatureSchedule *TemperatureSchedule::clone() const +{ + TemperatureSchedule *ret = new TemperatureSchedule(); + ret->setStartTime(m_startTime); + ret->setEndTime(m_endTime); + ret->setTemperature(m_temperature); + return ret; +} + +TemperatureDaySchedule::TemperatureDaySchedule(QObject *parent): + QAbstractListModel(parent) +{ +// qWarning() << "++++ DaySchedule" << this; + +} + +TemperatureDaySchedule::~TemperatureDaySchedule() +{ +// qWarning() << "---- DaySchedule" << this; +} + +QVariant TemperatureDaySchedule::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleStartTime: + return m_list.at(index.row())->startTime(); + case RoleEndTime: + return m_list.at(index.row())->endTime(); + case RoleTemperature: + return m_list.at(index.row())->temperature(); + } + return QVariant(); +} + +QHash TemperatureDaySchedule::roleNames() const +{ + return { + {RoleStartTime, "startTime"}, + {RoleEndTime, "endTime"}, + {RoleTemperature, "temperature"} + }; +} + +void TemperatureDaySchedule::clear() +{ + beginResetModel(); + qDeleteAll(m_list); + m_list.clear(); + endResetModel(); +} + +void TemperatureDaySchedule::addSchedule(TemperatureSchedule *schedule) +{ + schedule->setParent(this); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(schedule); + endInsertRows(); + emit countChanged(); +} + +TemperatureSchedule *TemperatureDaySchedule::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +TemperatureDaySchedule *TemperatureDaySchedule::clone() const +{ + // Note: passes ownership to caller (no parent)! + TemperatureDaySchedule *ret = new TemperatureDaySchedule(); + for (int i = 0; i < m_list.count(); i++) { + ret->addSchedule(m_list.at(i)->clone()); + } + return ret; +} + +TemperatureSchedule *TemperatureDaySchedule::createSchedule(const QTime &startTime, const QTime &endTime, double temperature) +{ + if (startTime >= endTime) { + qWarning() << "Starttime is greater endTime. Not creating schedule"; + return nullptr; + } + int idx = 0; + for (int i = 0; i < m_list.count(); i++) { + TemperatureSchedule *existing = m_list.at(i); + if (startTime < existing->startTime() && endTime < existing->endTime()) { + break; + } + if (startTime < existing->startTime() && endTime > existing->startTime()) { + qWarning() << "Collision detected. Not creating schedule"; + return nullptr; + } + if (startTime > existing->startTime() && startTime < existing->endTime()) { + qWarning() << "Collision detected. Not creating schedule"; + return nullptr; + } + idx = i + 1; + + } + TemperatureSchedule *newSchedule = new TemperatureSchedule(this); + newSchedule->setStartTime(startTime); + newSchedule->setEndTime(endTime); + newSchedule->setTemperature(temperature); + + beginInsertRows(QModelIndex(), idx, idx); + m_list.insert(idx, newSchedule); + endInsertRows(); + emit countChanged(); + return newSchedule; +} + +void TemperatureDaySchedule::removeSchedule(TemperatureSchedule *schedule) +{ + for (int i = 0; i < m_list.count(); i++) { + if (m_list.at(i) == schedule) { + beginRemoveRows(QModelIndex(), i, i); + m_list.takeAt(i)->deleteLater(); + endRemoveRows(); + emit countChanged(); + } + } +} + +TemperatureWeekSchedule::TemperatureWeekSchedule(QObject *parent): + QAbstractListModel(parent) +{ +// qWarning() << "++++ WeekSchedule" << this; + + for (int i = 0; i < 7; i++) { + m_list.append(new TemperatureDaySchedule(this)); + } +} + +TemperatureWeekSchedule::~TemperatureWeekSchedule() +{ +// qWarning() << "---- WeekSchedule" << this; +} + +TemperatureDaySchedule *TemperatureWeekSchedule::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +TemperatureWeekSchedule *TemperatureWeekSchedule::clone() const +{ + TemperatureWeekSchedule *weekSchedule = new TemperatureWeekSchedule(); + for (int day = 0; day < 7; day++) { + TemperatureDaySchedule *daySchedule = get(day); + for (int i = 0; i < daySchedule->rowCount(); i++) { + weekSchedule->get(day)->addSchedule(daySchedule->get(i)->clone()); + } + } + return weekSchedule; +} + diff --git a/experiences/airconditioning/temperatureschedule.h b/experiences/airconditioning/temperatureschedule.h new file mode 100644 index 00000000..14c62405 --- /dev/null +++ b/experiences/airconditioning/temperatureschedule.h @@ -0,0 +1,92 @@ +#ifndef TEMPERATURESCHEDULE_H +#define TEMPERATURESCHEDULE_H + +#include +#include +#include + +class TemperatureSchedule: public QObject +{ + Q_OBJECT + Q_PROPERTY(QTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged) + Q_PROPERTY(QTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged) + Q_PROPERTY(double temperature READ temperature WRITE setTemperature NOTIFY temperatureChanged) + +public: + explicit TemperatureSchedule(QObject *parent = nullptr); + ~TemperatureSchedule(); + + QTime startTime() const; + void setStartTime(const QTime &startTime); + + QTime endTime() const; + void setEndTime(const QTime &endTime); + + double temperature() const; + void setTemperature(double temperature); + + TemperatureSchedule *clone() const; +signals: + void startTimeChanged(); + void endTimeChanged(); + void temperatureChanged(); + +private: + QTime m_startTime; + QTime m_endTime; + double m_temperature = 0; + +}; + +class TemperatureDaySchedule: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleStartTime, + RoleEndTime, + RoleTemperature + }; + TemperatureDaySchedule(QObject *parent = nullptr); + ~TemperatureDaySchedule(); + + int rowCount(const QModelIndex & = QModelIndex()) const override { return m_list.count(); } + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void addSchedule(TemperatureSchedule *schedule); + Q_INVOKABLE TemperatureSchedule* get(int index) const; + + Q_INVOKABLE TemperatureDaySchedule *clone() const; // Passes ownership to caller + Q_INVOKABLE TemperatureSchedule* createSchedule(const QTime &startTime, const QTime &endTime, double temperature); + Q_INVOKABLE void removeSchedule(TemperatureSchedule *schedule); + Q_INVOKABLE void clear(); +signals: + void countChanged(); + +private: + QList m_list; +}; + +class TemperatureWeekSchedule: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount CONSTANT) +public: + TemperatureWeekSchedule(QObject *parent = nullptr); + ~TemperatureWeekSchedule(); + + int rowCount(const QModelIndex & = QModelIndex()) const override { return m_list.count(); } + QVariant data(const QModelIndex &, int) const override { return QVariant(); } + QHash roleNames() const override { return QHash(); } + + Q_INVOKABLE TemperatureDaySchedule* get(int index) const; + + Q_INVOKABLE TemperatureWeekSchedule *clone() const; // Passes ownership to caller + +private: + QList m_list; +}; + +#endif // TEMPERATURESCHEDULE_H diff --git a/experiences/airconditioning/zoneinfo.cpp b/experiences/airconditioning/zoneinfo.cpp new file mode 100644 index 00000000..c1272249 --- /dev/null +++ b/experiences/airconditioning/zoneinfo.cpp @@ -0,0 +1,216 @@ +#include "zoneinfo.h" + +ZoneInfo::ZoneInfo(const QUuid &id, QObject *parent) + : QObject{parent}, + m_id(id) +{ + m_weekSchedule = new TemperatureWeekSchedule(this); +} + +QUuid ZoneInfo::id() const +{ + return m_id; +} + +QString ZoneInfo::name() const +{ + return m_name; +} + +void ZoneInfo::setName(const QString &name) +{ + if (m_name != name) { + m_name = name; + emit nameChanged(); + } +} + +ZoneInfo::ZoneStatus ZoneInfo::zoneStatus() const +{ + return m_zoneStatus; +} + +void ZoneInfo::setZoneStatus(ZoneStatus zoneStatus) +{ + if (m_zoneStatus != zoneStatus) { + m_zoneStatus = zoneStatus; + emit zoneStatusChanged(); + } +} + +double ZoneInfo::currentSetpoint() const +{ + return m_currentSetpoint; +} + +void ZoneInfo::setCurrentSetpoint(double currentSetpoint) +{ + if (m_currentSetpoint != currentSetpoint) { + m_currentSetpoint = currentSetpoint; + emit currentSetpointChanged(); + } +} + +double ZoneInfo::standbySetpoint() const +{ + return m_standbySetpoint; +} + +void ZoneInfo::setStandbySetpoint(double standbySetpoint) +{ + if (m_standbySetpoint != standbySetpoint) { + m_standbySetpoint = standbySetpoint; + emit standbySetpointChanged(); + } +} + +double ZoneInfo::setpointOverride() const +{ + return m_setpointOverride; +} + +ZoneInfo::SetpointOverrideMode ZoneInfo::setpointOverrideMode() const +{ + return m_setpointOverrideMode; +} + +QDateTime ZoneInfo::setpointOverrideEnd() const +{ + return m_setpointOverrideEnd; +} + +void ZoneInfo::setSetpointOverride(double setpointOverride, SetpointOverrideMode mode, const QDateTime &end) +{ + if (m_setpointOverride != setpointOverride || m_setpointOverrideMode != mode || m_setpointOverrideEnd != end) { + m_setpointOverride = setpointOverride; + m_setpointOverrideMode = mode; + m_setpointOverrideEnd = end; + emit setpointOverrideChanged(); + } +} + +TemperatureWeekSchedule *ZoneInfo::weekSchedule() const +{ + return m_weekSchedule; +} + +QList ZoneInfo::thermostats() const +{ + return m_thermostats; +} + +void ZoneInfo::setThermostats(const QList &thermostats) +{ + if (m_thermostats != thermostats) { + m_thermostats = thermostats; + emit thermostatsChanged(); + } +} + +QList ZoneInfo::windowSensors() const +{ + return m_windowSensors; +} + +void ZoneInfo::setWindowSensors(const QList &windowSensors) +{ + if (m_windowSensors != windowSensors) { + m_windowSensors = windowSensors; + emit windowSensorsChanged(); + } +} + +QList ZoneInfo::indoorSensors() const +{ + return m_indoorSensors; +} + +void ZoneInfo::setIndoorSensors(const QList &indoorSensors) +{ + if (m_indoorSensors != indoorSensors) { + m_indoorSensors = indoorSensors; + emit indoorSensorsChanged(); + } +} + +QList ZoneInfo::outdoorSensors() const +{ + return m_outdoorSensors; +} + +void ZoneInfo::setOutdoorSensors(const QList &outdoorSensors) +{ + if (m_outdoorSensors != outdoorSensors) { + m_outdoorSensors = outdoorSensors; + emit outdoorSensorsChanged(); + } +} + +QVariant ZoneInfos::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleId: + return m_list.at(index.row())->id(); + case RoleName: + return m_list.at(index.row())->name(); + } + return QVariant(); +} + +QHash ZoneInfos::roleNames() const +{ + return { + {RoleId, "id"}, + {RoleName, "name"} + }; +} + +void ZoneInfos::addZoneInfo(ZoneInfo *zoneInfo) +{ + zoneInfo->setParent(this); + connect(zoneInfo, &ZoneInfo::nameChanged, this, [=](){ + QModelIndex idx = index(m_list.indexOf(zoneInfo)); + emit dataChanged(idx, idx, {RoleName}); + }); + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(zoneInfo); + endInsertRows(); + emit countChanged(); +} + +void ZoneInfos::removeZoneInfo(const QUuid &zoneId) +{ + int idx = -1; + for (int i = 0; i < m_list.count(); i++) { + ZoneInfo *zone = m_list.at(i); + if (zone->id() == zoneId) { + idx = i; + break; + } + } + if (idx < 0) { + return; + } + beginRemoveRows(QModelIndex(), idx, idx); + m_list.takeAt(idx)->deleteLater(); + endRemoveRows(); + emit countChanged(); +} + +ZoneInfo *ZoneInfos::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} + +ZoneInfo *ZoneInfos::getZoneInfo(const QUuid &zoneId) const +{ + foreach (ZoneInfo *zone, m_list) { + if (zone->id() == zoneId) { + return zone; + } + } + return nullptr; +} diff --git a/experiences/airconditioning/zoneinfo.h b/experiences/airconditioning/zoneinfo.h new file mode 100644 index 00000000..67475472 --- /dev/null +++ b/experiences/airconditioning/zoneinfo.h @@ -0,0 +1,138 @@ +#ifndef ZONEINFO_H +#define ZONEINFO_H + +#include +#include +#include + +#include "temperatureschedule.h" + +class ZoneInfo : public QObject +{ + Q_OBJECT + Q_PROPERTY(QUuid id READ id CONSTANT) + Q_PROPERTY(ZoneStatus zoneStatus READ zoneStatus NOTIFY zoneStatusChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(double currentSetpoint READ currentSetpoint NOTIFY currentSetpointChanged) + Q_PROPERTY(double standbySetpoint READ standbySetpoint NOTIFY standbySetpointChanged) + Q_PROPERTY(double setpointOverride READ setpointOverride NOTIFY setpointOverrideChanged) + Q_PROPERTY(SetpointOverrideMode setpointOverrideMode READ setpointOverrideMode NOTIFY setpointOverrideChanged) + Q_PROPERTY(QDateTime setpointOverrideEnd READ setpointOverrideEnd NOTIFY setpointOverrideChanged) + Q_PROPERTY(TemperatureWeekSchedule* weekSchedule READ weekSchedule CONSTANT) + Q_PROPERTY(QList thermostats READ thermostats NOTIFY thermostatsChanged) + Q_PROPERTY(QList windowSensors READ windowSensors NOTIFY windowSensorsChanged) + Q_PROPERTY(QList indoorSensors READ indoorSensors NOTIFY indoorSensorsChanged) + Q_PROPERTY(QList outdoorSensors READ outdoorSensors NOTIFY outdoorSensorsChanged) +public: + enum ZoneStatusFlag { + ZoneStatusFlagNone = 0x00, + ZoneStatusFlagTimeScheduleActive = 0x01, + ZoneStatusFlagSetpointOverrideActive = 0x02, + ZoneStatusFlagWindowOpen = 0x10, + ZoneStatusFlagBadAir = 0x20, + ZoneStatusFlagHighHumidity = 0x40 + }; + Q_ENUM(ZoneStatusFlag) + Q_DECLARE_FLAGS(ZoneStatus, ZoneStatusFlag) +// Q_DECLARE_OPERATORS_FOR_FLAGS(ZoneStatus) + Q_FLAG(ZoneStatus) + + enum SetpointOverrideMode { + SetpointOverrideModeNone = 0, + SetpointOverrideModeTimed, + SetpointOverrideModeUnlimited, + SetpointOverrideModeEventual + }; + Q_ENUM(SetpointOverrideMode) + + explicit ZoneInfo(const QUuid &id, QObject *parent = nullptr); + + QUuid id() const; + + QString name() const; + void setName(const QString &name); + + ZoneStatus zoneStatus() const; + void setZoneStatus(ZoneStatus zoneStatus); + + double currentSetpoint() const; + void setCurrentSetpoint(double currentSetpoint); + + double standbySetpoint() const; + void setStandbySetpoint(double standbySetpoint); + + double setpointOverride() const; + SetpointOverrideMode setpointOverrideMode() const; + QDateTime setpointOverrideEnd() const; + void setSetpointOverride(double setpointOverride, SetpointOverrideMode mode, const QDateTime &end); + + TemperatureWeekSchedule *weekSchedule() const; + + QList thermostats() const; + void setThermostats(const QList &thermostats); + + QList windowSensors() const; + void setWindowSensors(const QList &windowSensors); + + QList indoorSensors() const; + void setIndoorSensors(const QList &indoorSensors); + + QList outdoorSensors() const; + void setOutdoorSensors(const QList &outdoorSensors); + +signals: + void nameChanged(); + void zoneStatusChanged(); + void currentSetpointChanged(); + void standbySetpointChanged(); + void setpointOverrideChanged(); + void thermostatsChanged(); + void windowSensorsChanged(); + void indoorSensorsChanged(); + void outdoorSensorsChanged(); + +private: + QUuid m_id; + ZoneStatus m_zoneStatus = ZoneStatusFlagNone; + QString m_name; + double m_currentSetpoint = 18; + double m_standbySetpoint = 18; + double m_setpointOverride = 18; + SetpointOverrideMode m_setpointOverrideMode = SetpointOverrideModeNone; + QDateTime m_setpointOverrideEnd; + TemperatureWeekSchedule *m_weekSchedule = nullptr; + QList m_thermostats; + QList m_windowSensors; + QList m_indoorSensors; + QList m_outdoorSensors; +}; + +class ZoneInfos: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) +public: + enum Roles { + RoleId, + RoleName, + }; + ZoneInfos(QObject *parent = nullptr): QAbstractListModel(parent) {} + + int rowCount(const QModelIndex & = QModelIndex()) const override { return m_list.count(); } + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void addZoneInfo(ZoneInfo *zoneInfo); + void removeZoneInfo(const QUuid &zoneId); + + Q_INVOKABLE ZoneInfo* get(int index) const; + Q_INVOKABLE ZoneInfo* getZoneInfo(const QUuid &zoneId) const; + +signals: + void countChanged(); + +private: + QList m_list; +}; + +#endif // ZONEINFO_H diff --git a/experiences/experiences.pro b/experiences/experiences.pro new file mode 100644 index 00000000..298fcbc4 --- /dev/null +++ b/experiences/experiences.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs + +SUBDIRS += airconditioning diff --git a/libnymea-app/connection/nymeahost.h b/libnymea-app/connection/nymeahost.h index e8a54d52..bbeb83f5 100644 --- a/libnymea-app/connection/nymeahost.h +++ b/libnymea-app/connection/nymeahost.h @@ -35,7 +35,6 @@ #include #include #include -#include #include #include #include diff --git a/libnymea-app/energy/energylogs.cpp b/libnymea-app/energy/energylogs.cpp index d59a5c1c..03170b0d 100644 --- a/libnymea-app/energy/energylogs.cpp +++ b/libnymea-app/energy/energylogs.cpp @@ -284,6 +284,7 @@ void EnergyLogs::getLogsResponse(int commandId, const QVariantMap ¶ms) double minValue = 0, maxValue = 0; qCDebug(dcEnergyLogs()) << "Logs response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); QList entries = unpackEntries(params, &minValue, &maxValue); + qCDebug(dcEnergyLogs()) << "Energy logs received" << entries.count(); if (!entries.isEmpty()) { if (m_list.isEmpty()) { @@ -455,7 +456,7 @@ void EnergyLogs::fetchLogs() m_fetchingData = true; fetchingDataChanged(); - qCDebug(dcEnergyLogs()) << "Fetching" << m_startTime << m_endTime; + qCDebug(dcEnergyLogs()) << "Fetching energy logs:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); m_engine->jsonRpcClient()->sendCommand("Energy.Get" + logsName(), params, this, "getLogsResponse"); } diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index c79bc980..d6bde858 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -67,6 +67,7 @@ #include "models/logsmodelng.h" #include "models/barseriesadapter.h" #include "models/xyseriesadapter.h" +#include "models/boolseriesadapter.h" #include "models/interfacesproxy.h" #include "configuration/nymeaconfiguration.h" #include "configuration/serverconfiguration.h" @@ -147,6 +148,11 @@ #include +namespace Nymea +{ +namespace Core +{ + static QObject* interfacesModel_provider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) @@ -270,6 +276,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "LogEntry", "Get them from LogsModel"); qmlRegisterType(uri, 1, 0, "BarSeriesAdapter"); qmlRegisterType(uri, 1, 0, "XYSeriesAdapter"); + qmlRegisterType(uri, 1, 0, "BoolSeriesAdapter"); qmlRegisterUncreatableType(uri, 1, 0, "TagsManager", "Get it from Engine"); qmlRegisterUncreatableType(uri, 1, 0, "Tags", "Get it from TagsManager"); @@ -382,5 +389,6 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "SortFilterProxyModel"); } - +} +} #endif // LIBNYMEAAPPCORE_H diff --git a/libnymea-app/libnymea-app.pri b/libnymea-app/libnymea-app.pri index 220480ca..1c5eae50 100644 --- a/libnymea-app/libnymea-app.pri +++ b/libnymea-app/libnymea-app.pri @@ -27,6 +27,7 @@ SOURCES += \ $$PWD/energy/powerbalancelogs.cpp \ $$PWD/energy/thingpowerlogs.cpp \ $$PWD/connection/tunnelproxytransport.cpp \ + $$PWD/models/boolseriesadapter.cpp \ $$PWD/models/scriptsproxymodel.cpp \ $$PWD/pluginconfigmanager.cpp \ $$PWD/tagwatcher.cpp \ @@ -190,6 +191,7 @@ HEADERS += \ $$PWD/energy/powerbalancelogs.h \ $$PWD/energy/thingpowerlogs.h \ $$PWD/connection/tunnelproxytransport.h \ + $$PWD/models/boolseriesadapter.h \ $$PWD/models/scriptsproxymodel.h \ $$PWD/pluginconfigmanager.h \ $$PWD/tagwatcher.h \ diff --git a/libnymea-app/models/boolseriesadapter.cpp b/libnymea-app/models/boolseriesadapter.cpp new file mode 100644 index 00000000..20104f06 --- /dev/null +++ b/libnymea-app/models/boolseriesadapter.cpp @@ -0,0 +1,146 @@ +#include "boolseriesadapter.h" + +BoolSeriesAdapter::BoolSeriesAdapter(QObject *parent) + : QObject{parent} +{ + +} + +LogsModel *BoolSeriesAdapter::logsModel() const +{ + return m_model; +} + +void BoolSeriesAdapter::setLogsModel(LogsModel *logsModel) +{ + if (m_model != logsModel) { + m_model = logsModel; + emit logsModelChanged(); +// update(); + connect(logsModel, &LogsModel::logEntryAdded, this, &BoolSeriesAdapter::logEntryAdded); + } + +} + +QtCharts::QXYSeries *BoolSeriesAdapter::xySeries() const +{ + return m_series; +} + +void BoolSeriesAdapter::setXySeries(QtCharts::QXYSeries *series) +{ + if (m_series != series) { + m_series = series; + emit xySeriesChanged(); + + m_series->clear(); + m_series->append(QDateTime::currentDateTime().addYears(1).toMSecsSinceEpoch(), 0); + m_series->append(0, 0); + qWarning() << "Initialized series" << m_series->count(); + } +} + +bool BoolSeriesAdapter::inverted() const +{ + return m_inverted; +} + +void BoolSeriesAdapter::setInverted(bool inverted) +{ + if (m_inverted != inverted) { + m_inverted = inverted; + emit invertedChanged(); + } +} + +void BoolSeriesAdapter::logEntryAdded(LogEntry *entry) +{ + if (!m_series) { + return; + } + + int idx = findIndex(entry->timestamp().toMSecsSinceEpoch()); + qreal value = entry->value().toBool() != m_inverted ? 1 : 0; + + if (m_series->count() >= 2000) { + qCWarning(dcLogEngine()) << "Thing logs too excessively. Discarding entry."; + return; + } + +// QDebug dbg = qWarning(); +// dbg << "List before insert:\n"; +// for (int i = 0; i < m_series->count(); i++) { +// dbg << i << QDateTime::fromMSecsSinceEpoch(m_series->at(i).x()) << m_series->at(i).y() << "\n"; +// } + +// qWarning() << "Inserting" << entry->timestamp() << entry->value() << "real value:" << value << "at" << idx << "total:" << m_series->count(); + // We're keeping a fake entry at the beginning (timestamp 0) with a static value of 0 + // and on in the beginning (+1 year from now) for which we'll update the value according to + // the newest real entry to continue painting the last value. + + // Update the future value if this is the new newest real entry + if (idx == 1) { + m_series->replace(0, QDateTime::currentDateTime().addYears(1).toMSecsSinceEpoch(), value); + } + // If the next older entry is different than this, first insert the other value right before this one + if (qFuzzyIsNull(m_series->at(idx).y()) != qFuzzyIsNull(value)) { + m_series->insert(idx, QPointF(entry->timestamp().toMSecsSinceEpoch() - 1, !value)); + } + + m_series->insert(idx, QPointF(entry->timestamp().toMSecsSinceEpoch(), value)); + + // If the next newer entry is differnt than this, also insert this value right before the next one + if (qFuzzyIsNull(m_series->at(idx-1).y()) != qFuzzyIsNull(value)) { + m_series->insert(idx, QPointF(m_series->at(idx-1).x() - 1, value)); + } + +// qWarning() << "***** series count" << m_series->count(); +// dbg << "List after insert:\n"; +// for (int i = 0; i < m_series->count(); i++) { +// dbg << i << QDateTime::fromMSecsSinceEpoch(m_series->at(i).x()) << m_series->at(i).y() << "\n"; +// } +} + +quint64 BoolSeriesAdapter::findIndex(qulonglong timestamp) +{ + if (m_series->count() == 2) { + return 1; + } + + // In 99.9% of the cases we'll be prepending (adding live entries) or appending (fetching history) + if (timestamp < m_series->at(m_series->count() - 2).x()) { + return m_series->count() - 1; + } + if (timestamp > m_series->at(1).x()) { + return 1; + } + + // If for any reason a entry in the middle is added (can't think of one but hey), a binary search will probably do. + int idx = m_series->count() / 2; + int range = idx; + int i = 0; + while (true) { + qWarning() << "CNT:" << m_series->count() + << "first:" << QDateTime::fromMSecsSinceEpoch(m_series->at(1).x()) + << "last:" << QDateTime::fromMSecsSinceEpoch(m_series->at(m_series->count()- 2).x()) + << "current:" << idx << QDateTime::fromMSecsSinceEpoch(m_series->at(idx).x()) + << "search:" << QDateTime::fromMSecsSinceEpoch(timestamp); + if (timestamp >= m_series->at(idx).x() && timestamp < m_series->at(idx-1).x()) { + return idx; + } + if (timestamp <= m_series->at(idx).x() && timestamp > m_series->at(idx+1).x()) { + return idx+1; + } + range = qMax(1, range / 2); + if (timestamp > m_series->at(idx).x()) { + idx = idx - range; + } else { + idx = idx + range; + } + + if (i++ > 2000) { + break; + } + } + return 1; +} diff --git a/libnymea-app/models/boolseriesadapter.h b/libnymea-app/models/boolseriesadapter.h new file mode 100644 index 00000000..05a2aabb --- /dev/null +++ b/libnymea-app/models/boolseriesadapter.h @@ -0,0 +1,49 @@ +#ifndef BOOLSERIESADAPTER_H +#define BOOLSERIESADAPTER_H + +#include "logsmodel.h" + +#include +#include + +class BoolSeriesAdapter : public QObject +{ + Q_OBJECT + Q_PROPERTY(LogsModel* logsModel READ logsModel WRITE setLogsModel NOTIFY logsModelChanged) + Q_PROPERTY(QtCharts::QXYSeries* xySeries READ xySeries WRITE setXySeries NOTIFY xySeriesChanged) + + Q_PROPERTY(bool inverted READ inverted WRITE setInverted NOTIFY invertedChanged) + +public: + explicit BoolSeriesAdapter(QObject *parent = nullptr); + + LogsModel* logsModel() const; + void setLogsModel(LogsModel *logsModel); + + QtCharts::QXYSeries* xySeries() const; + void setXySeries(QtCharts::QXYSeries *series); + + bool inverted() const; + void setInverted(bool inverted); + +signals: + void xySeriesChanged(); + void logsModelChanged(); + void invertedChanged(); + +private slots: + void logEntryAdded(LogEntry *entry); + +private: + qreal calculateSampleValue(int index); + + quint64 findIndex(qulonglong timestamp); + +private: + LogsModel* m_model = nullptr; + QtCharts::QXYSeries* m_series = nullptr; + bool m_inverted = false; + +}; + +#endif // BOOLSERIESADAPTER_H diff --git a/libnymea-app/models/logsmodel.cpp b/libnymea-app/models/logsmodel.cpp index d2578a77..30280f12 100644 --- a/libnymea-app/models/logsmodel.cpp +++ b/libnymea-app/models/logsmodel.cpp @@ -202,7 +202,7 @@ void LogsModel::setViewStartTime(const QDateTime &viewStartTime) m_viewStartTime = viewStartTime; emit viewStartTimeChanged(); if (m_list.count() == 0 || m_list.last()->timestamp() > m_viewStartTime) { - if (m_canFetchMore) { + if (m_canFetchMore) { fetchMore(); } } @@ -296,7 +296,10 @@ void LogsModel::logsReply(int /*commandId*/, const QVariantMap &data) int offset = data.value("offset").toInt() + m_generatedEntries; int count = data.value("count").toInt(); -// qDebug() << qUtf8Printable(QJsonDocument::fromVariant(data).toJson()); + qCInfo(dcLogEngine()) << objectName() << "Logs reply:" << m_fetchStartTime.msecsTo(QDateTime::currentDateTime()); + qCDebug(dcLogEngine()) << objectName() << qUtf8Printable(QJsonDocument::fromVariant(data).toJson()); + + m_fetchStartTime = QDateTime::currentDateTime(); QList newBlock; QList logEntries = data.value("logEntries").toList(); @@ -314,9 +317,9 @@ void LogsModel::logsReply(int /*commandId*/, const QVariantMap &data) bool stopProcessing = false; if (m_viewStartTime.isValid() && timeStamp.addSecs(-60) < m_viewStartTime) { - timeStamp = m_viewStartTime.addSecs(-60); +// timeStamp = m_viewStartTime.addSecs(-60); stopProcessing = true; - m_generatedEntries++; +// m_generatedEntries++; } LogEntry *entry = new LogEntry(timeStamp, value, thingId, typeId, loggingSource, loggingEventType, errorCode, this); newBlock.append(entry); @@ -326,9 +329,9 @@ void LogsModel::logsReply(int /*commandId*/, const QVariantMap &data) } } -// qCDebug(dcLogEngine()) << objectName() << "Received logs from" << offset << "to" << offset + count << "Actual count:" << newBlock.count(); + qCInfo(dcLogEngine()) << objectName() << "Received logs from" << offset << "to" << offset + count << "Actual count:" << newBlock.count(); - if (count < m_blockSize) { + if (newBlock.count() == count && count < m_blockSize) { m_canFetchMore = false; } @@ -352,8 +355,10 @@ void LogsModel::logsReply(int /*commandId*/, const QVariantMap &data) m_busyInternal = false; + qCInfo(dcLogEngine()) << objectName() << "Logs fetched" << m_fetchStartTime.msecsTo(QDateTime::currentDateTime()); + if (m_viewStartTime.isValid() && m_list.count() > 0 && m_list.last()->timestamp() > m_viewStartTime && m_canFetchMore) { - qCDebug(dcLogEngine()) << objectName() << "Fetching more because of viewStartTime" << m_viewStartTime.toString() << "last" << m_list.last()->timestamp().toString(); + qCInfo(dcLogEngine()) << objectName() << "Fetching more because of viewStartTime" << m_viewStartTime.toString() << "last" << m_list.last()->timestamp().toString(); fetchMore(); } else { m_busy = false; @@ -410,9 +415,11 @@ void LogsModel::fetchMore(const QModelIndex &parent) params.insert("limit", m_blockSize); params.insert("offset", m_list.count() - m_generatedEntries); -// qDebug() << "Fetching logs from" << m_startTime.toString() << "to" << m_endTime.toString() << "with offset" << m_list.count() << "and limit" << m_blockSize; + qCInfo(dcLogEngine()) << "Fetching logs from:" << m_list.count() - m_generatedEntries << "max" << m_blockSize; + qCDebug(dcLogEngine()) << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply"); + m_fetchStartTime = QDateTime::currentDateTime(); // qDebug() << "GetLogEntries called"; } diff --git a/libnymea-app/models/logsmodel.h b/libnymea-app/models/logsmodel.h index 47821636..42962f14 100644 --- a/libnymea-app/models/logsmodel.h +++ b/libnymea-app/models/logsmodel.h @@ -144,6 +144,7 @@ protected: int m_generatedEntries = 0; + QDateTime m_fetchStartTime; }; #endif // LOGSMODEL_H diff --git a/libnymea-app/models/logsmodelng.cpp b/libnymea-app/models/logsmodelng.cpp index 818fe697..f4350e8e 100644 --- a/libnymea-app/models/logsmodelng.cpp +++ b/libnymea-app/models/logsmodelng.cpp @@ -247,6 +247,67 @@ LogEntry *LogsModelNg::get(int index) const return nullptr; } +LogEntry *LogsModelNg::findClosest(const QDateTime &dateTime) const +{ +// qWarning() << "********************Finding closest for:" << dateTime.toString(); +// foreach (LogEntry *entry, m_list) { +// qWarning() << "List entry:" << entry->timestamp().toString(); +// } + if (m_list.isEmpty()) { +// qWarning() << "No entries here..."; + return nullptr; + } + int newest = 0; + int oldest = m_list.count() - 1; + LogEntry *entry = nullptr; + int step = 0; + + LogEntry *allTimeOldestEntry = m_list.at(oldest); + if (dateTime < allTimeOldestEntry->timestamp()) { +// qWarning() << "All time oldest is newer than searched"; + return nullptr; + } +// qWarning() << "Oldest:" << oldest << "newest:" << newest << "step" << step << "count" << m_list.count(); + while (oldest >= newest && step < m_list.count()) { + LogEntry *oldestEntry = m_list.at(oldest); + LogEntry *newestEntry = m_list.at(newest); + int middle = (oldest - newest) / 2 + newest; + LogEntry *middleEntry = m_list.at(middle); +// qWarning() << "Oldest:" << oldest << oldestEntry->timestamp().toString() << oldestEntry->value() << "Middle:" << middle << middleEntry->timestamp().toString() << middleEntry->value() << "Newest:" << newest << newestEntry->timestamp().toString() << newestEntry->value() << ":" << (oldest - newest); + if (dateTime <= oldestEntry->timestamp()) { +// qWarning() << "Returning oldest"; + return oldestEntry; + } + if (dateTime >= newestEntry->timestamp()) { +// qWarning() << "Returning newest"; + return newestEntry; + } + + if (dateTime == middleEntry->timestamp()) { +// qWarning() << "Returning middle"; + return middleEntry; + } + + if (dateTime < middleEntry->timestamp()) { + newest = middle; + } else { + oldest = middle; + } + + if (oldest - newest == 1) { + if (oldest > middle) { +// qWarning() << "EOL. Returning oldest"; + return oldestEntry; + } else { +// qWarning() << "EOL. Returning middle"; + return middleEntry; + } + } + step++; + } + return entry; +} + void LogsModelNg::logsReply(int commandId, const QVariantMap &data) { Q_UNUSED(commandId) diff --git a/libnymea-app/models/logsmodelng.h b/libnymea-app/models/logsmodelng.h index b6b8649c..c6b7c441 100644 --- a/libnymea-app/models/logsmodelng.h +++ b/libnymea-app/models/logsmodelng.h @@ -107,6 +107,7 @@ public: QVariant maxValue() const; Q_INVOKABLE LogEntry *get(int index) const; + Q_INVOKABLE LogEntry *findClosest(const QDateTime &dateTime) const; protected: virtual void fetchMore(const QModelIndex &parent = QModelIndex()) override; diff --git a/libnymea-app/models/xyseriesadapter.cpp b/libnymea-app/models/xyseriesadapter.cpp index a86d3706..7687c40f 100644 --- a/libnymea-app/models/xyseriesadapter.cpp +++ b/libnymea-app/models/xyseriesadapter.cpp @@ -35,6 +35,8 @@ void XYSeriesAdapter::setXySeries(QtCharts::QXYSeries *series) if (m_series != series) { m_series = series; emit xySeriesChanged(); + + ensureSamples(QDateTime::currentDateTime(), QDateTime::currentDateTime().addMSecs(2 * 60000)); } } @@ -135,14 +137,14 @@ qreal XYSeriesAdapter::minValue() const void XYSeriesAdapter::ensureSamples(const QDateTime &from, const QDateTime &to) { -// qWarning() << "Ensuring samples:" << from.toString("yyyy-MM-dd hh:mm:ss") << to.toString("yyyy-MM-dd hh:mm:ss"); if (!m_series) { return; } if (m_samples.isEmpty()) { Sample *sample = new Sample(); - sample->timestamp = from.addSecs(m_sampleRate); + sample->timestamp = from.addSecs(m_sampleRate / 2); +// qWarning() << "Added first" << from << sample->timestamp; m_newestSample = sample->timestamp; m_oldestSample = m_newestSample; m_samples.append(sample); @@ -152,18 +154,26 @@ void XYSeriesAdapter::ensureSamples(const QDateTime &from, const QDateTime &to) while (to > m_newestSample) { Sample *sample = new Sample(); sample->timestamp = m_newestSample.addSecs(m_sampleRate); + Sample *oldNewest = m_samples.first(); + if (oldNewest->entries.count() > 0) { + sample->startingPoint = oldNewest->entries.last(); + } else if (oldNewest->startingPoint != nullptr) { + sample->startingPoint = oldNewest->startingPoint; + } m_newestSample = sample->timestamp; m_samples.prepend(sample); - m_series->insert(0, QPointF(sample->timestamp.toMSecsSinceEpoch(), 0)); + m_series->insert(0, QPointF(sample->timestamp.toMSecsSinceEpoch(), m_series->at(0).y())); } - while (from < m_oldestSample.addSecs(m_sampleRate)) { + while (from < m_oldestSample.addSecs(-m_sampleRate)) { +// qWarning() << "Added one before" << from << m_oldestSample.addMSecs(-m_sampleRate) << m_oldestSample; Sample *sample = new Sample(); sample->timestamp = m_oldestSample.addSecs(-m_sampleRate); m_oldestSample = sample->timestamp; m_samples.append(sample); m_series->append(sample->timestamp.toMSecsSinceEpoch(), 0); } +// qWarning() << "Ensuring samples:" << from.toString("yyyy-MM-dd hh:mm:ss") << to.toString("yyyy-MM-dd hh:mm:ss") << "Oldest:" << m_oldestSample.toString("yyyy-MM-dd hh:mm:ss") << "Newest:" << m_newestSample.toString("yyyy-MM-dd hh:mm:ss") ; } void XYSeriesAdapter::logEntryAdded(LogEntry *entry) @@ -179,7 +189,7 @@ void XYSeriesAdapter::logEntryAdded(LogEntry *entry) qCWarning(dcLogEngine) << objectName() << "Overflowing integer size for XYSeriesAdapter!"; return; } -// qCDebug(dcLogEngine()) << objectName() << "Inserting sample at:" << idx << entry->timestamp(); +// qWarning() << objectName() << "Inserting sample at:" << idx << entry->timestamp().toString("yyyy-MM-dd hh:mm:ss"); Sample *sample = m_samples.at(idx); LogEntry *oldLast = nullptr; // In theory we'd need to insert sorted, but only the last one actually matters for subsequent samples @@ -194,7 +204,7 @@ void XYSeriesAdapter::logEntryAdded(LogEntry *entry) qreal value = calculateSampleValue(idx); m_series->replace(idx, sample->timestamp.toMSecsSinceEpoch(), value); -// qWarning() << "sample value added" << idx << entry->timestamp().time().toString("hh:mm:ss") << value; +// qWarning() << "sample value updated" << idx << sample->timestamp.toString("yyyy-MM-dd hh:mm:ss") << value; if (value < m_minValue) { m_minValue = value; diff --git a/libnymea-app/models/xyseriesadapter.h b/libnymea-app/models/xyseriesadapter.h index fe82c77a..90ebd7b4 100644 --- a/libnymea-app/models/xyseriesadapter.h +++ b/libnymea-app/models/xyseriesadapter.h @@ -75,7 +75,7 @@ private: class Sample { public: QDateTime timestamp; // The timestamp where this sample *ends* - QList entries; // all log entries in this sample, that is, from timestamp - smaple size to timestamp + QVector entries; // all log entries in this sample, that is, from timestamp - smaple size to timestamp LogEntry *startingPoint = nullptr; // the starting point for the sample. Normally the last entry of the previous sample }; LogsModel* m_model = nullptr; diff --git a/libnymea-app/things.cpp b/libnymea-app/things.cpp index 1d6ccc44..7c1e027c 100644 --- a/libnymea-app/things.cpp +++ b/libnymea-app/things.cpp @@ -95,6 +95,8 @@ QVariant Things::data(const QModelIndex &index, int role) const return thing->thingClass()->interfaces(); case RoleBaseInterface: return thing->thingClass()->baseInterface(); + case RoleMainInterface: + return thing->thingClass()->interfaces().count() > 0 ? thing->thingClass()->interfaces().first() : ""; } return QVariant(); } @@ -167,5 +169,6 @@ QHash Things::roleNames() const roles[RoleSetupDisplayMessage] = "setupDisplayMessage"; roles[RoleInterfaces] = "interfaces"; roles[RoleBaseInterface] = "baseInterface"; + roles[RoleMainInterface] = "mainInterface"; return roles; } diff --git a/libnymea-app/things.h b/libnymea-app/things.h index 44c64b6e..4b706432 100644 --- a/libnymea-app/things.h +++ b/libnymea-app/things.h @@ -52,7 +52,8 @@ public: RoleSetupStatus, RoleSetupDisplayMessage, RoleInterfaces, - RoleBaseInterface + RoleBaseInterface, + RoleMainInterface }; Q_ENUM(Roles) diff --git a/libnymea-app/thingsproxy.cpp b/libnymea-app/thingsproxy.cpp index 292e2280..1d13692a 100644 --- a/libnymea-app/thingsproxy.cpp +++ b/libnymea-app/thingsproxy.cpp @@ -37,6 +37,9 @@ ThingsProxy::ThingsProxy(QObject *parent) : QSortFilterProxyModel(parent) { setSortRole(Things::RoleName); + connect(this, &ThingsProxy::countChanged, this, [=](){ + m_oldCount = rowCount(); + }); } Engine *ThingsProxy::engine() const @@ -67,7 +70,7 @@ void ThingsProxy::setEngine(Engine *engine) connect(sourceModel(), SIGNAL(countChanged()), this, SIGNAL(countChanged())); connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this]() { // Only invalidate the filter if we're actually interested in state changes - if (!m_sortStateName.isEmpty() || m_filterBatteryCritical || m_filterDisconnected || m_filterUpdates || m_filterSetupFailed) { + if (!m_sortStateName.isEmpty() || m_filterBatteryCritical || m_filterDisconnected || m_filterUpdates || m_filterSetupFailed || !m_stateFilter.isEmpty()) { invalidateFilterInternal(); } }); @@ -264,6 +267,28 @@ void ThingsProxy::setHiddenThingClassIds(const QStringList &hiddenThingClassIds) } } +QStringList ThingsProxy::shownThingIds() const +{ + QStringList ret; + foreach (const QUuid &id, m_shownThingIds) { + ret << id.toString(); + } + return ret; +} + +void ThingsProxy::setShownThingIds(const QStringList &shownThingIds) +{ + QList uuids; + foreach (const QString &str, shownThingIds) { + uuids << QUuid(str); + } + if (m_shownThingIds != uuids) { + m_shownThingIds = uuids; + emit shownThingIdsChanged(); + invalidateFilterInternal(); + } +} + QStringList ThingsProxy::hiddenThingIds() const { QStringList ret; @@ -455,6 +480,21 @@ void ThingsProxy::setParamsFilter(const QVariantMap ¶msFilter) } } +QVariantMap ThingsProxy::stateFilter() const +{ + return m_stateFilter; +} + +void ThingsProxy::setStateFilter(const QVariantMap &stateFilter) +{ + if (m_stateFilter != stateFilter) { + m_stateFilter = stateFilter; + emit stateFilterChanged(); + + invalidateFilterInternal(); + } +} + bool ThingsProxy::groupByInterface() const { return m_groupByInterface; @@ -526,9 +566,8 @@ int ThingsProxy::indexOf(Thing *thing) const void ThingsProxy::invalidateFilterInternal() { - int oldCount = rowCount(); invalidateFilter(); - if (oldCount != rowCount()) { + if (m_oldCount != rowCount()) { emit countChanged(); } } @@ -622,13 +661,12 @@ bool ThingsProxy::filterAcceptsRow(int source_row, const QModelIndex &source_par } ThingClass *thingClass = m_engine->thingManager()->thingClasses()->getThingClass(thing->thingClassId()); -// qDebug() << "Checking thing" << thingClass->name() << thingClass->interfaces(); if (!m_shownInterfaces.isEmpty()) { bool foundMatch = false; foreach (const QString &filterInterface, m_shownInterfaces) { if (thingClass->interfaces().contains(filterInterface)) { foundMatch = true; - continue; + break; } } if (!foundMatch) { @@ -655,6 +693,12 @@ bool ThingsProxy::filterAcceptsRow(int source_row, const QModelIndex &source_par return false; } + if (!m_shownThingIds.isEmpty()) { + if (!m_shownThingIds.contains(thing->id())) { + return false; + } + } + if (m_hiddenThingIds.contains(thing->id())) { return false; } @@ -732,5 +776,14 @@ bool ThingsProxy::filterAcceptsRow(int source_row, const QModelIndex &source_par } } + if (!m_stateFilter.isEmpty()) { + foreach (const QString &stateName, m_stateFilter.keys()) { + State *state = thing->stateByName(stateName); + if (!state || state->value() != m_stateFilter.value(stateName)) { + return false; + } + } + } + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } diff --git a/libnymea-app/thingsproxy.h b/libnymea-app/thingsproxy.h index 5317f7d8..f4f216cd 100644 --- a/libnymea-app/thingsproxy.h +++ b/libnymea-app/thingsproxy.h @@ -56,6 +56,7 @@ class ThingsProxy : public QSortFilterProxyModel Q_PROPERTY(QStringList shownThingClassIds READ shownThingClassIds WRITE setShownThingClassIds NOTIFY shownThingClassIdsChanged) Q_PROPERTY(QStringList hiddenThingClassIds READ hiddenThingClassIds WRITE setHiddenThingClassIds NOTIFY hiddenThingClassIdsChanged) + Q_PROPERTY(QStringList shownThingIds READ shownThingIds WRITE setShownThingIds NOTIFY shownThingIdsChanged) Q_PROPERTY(QStringList hiddenThingIds READ hiddenThingIds WRITE setHiddenThingIds NOTIFY hiddenThingIdsChanged) Q_PROPERTY(QString requiredEventName READ requiredEventName WRITE setRequiredEventName NOTIFY requiredEventNameChanged) @@ -81,6 +82,9 @@ class ThingsProxy : public QSortFilterProxyModel // A map of paramName:value pairs, all given need to match Q_PROPERTY(QVariantMap paramsFilter READ paramsFilter WRITE setParamsFilter NOTIFY paramsFilterChanged) + // A map of stateName:value pairs, all given need to match + Q_PROPERTY(QVariantMap stateFilter READ stateFilter WRITE setStateFilter NOTIFY stateFilterChanged) + Q_PROPERTY(bool groupByInterface READ groupByInterface WRITE setGroupByInterface NOTIFY groupByInterfaceChanged) // If set, sorting will happen for the value of the given state. Make sure the filter is set to contain only things that have the given state @@ -128,6 +132,9 @@ public: QStringList hiddenThingClassIds() const; void setHiddenThingClassIds(const QStringList &hiddenThingClassIds); + QStringList shownThingIds() const; + void setShownThingIds(const QStringList &shownThingIds); + QStringList hiddenThingIds() const; void setHiddenThingIds(const QStringList &hiddenThingIds); @@ -167,6 +174,9 @@ public: QVariantMap paramsFilter() const; void setParamsFilter(const QVariantMap ¶msFilter); + QVariantMap stateFilter() const; + void setStateFilter(const QVariantMap &stateFilter); + bool groupByInterface() const; void setGroupByInterface(bool groupByInterface); @@ -192,6 +202,7 @@ signals: void nameFilterChanged(); void shownThingClassIdsChanged(); void hiddenThingClassIdsChanged(); + void shownThingIdsChanged(); void hiddenThingIdsChanged(); void requiredEventNameChanged(); void requiredStateNameChanged(); @@ -205,6 +216,7 @@ signals: void filterSetupFailedChanged(); void filterUpdatesChanged(); void paramsFilterChanged(); + void stateFilterChanged(); void groupByInterfaceChanged(); void sortStateNameChanged(); void sortOrderChanged(); @@ -228,6 +240,7 @@ private: QString m_nameFilter; QList m_shownThingClassIds; QList m_hiddenThingClassIds; + QList m_shownThingIds; QList m_hiddenThingIds; QString m_requiredEventName; @@ -245,11 +258,14 @@ private: bool m_filterUpdates = false; QVariantMap m_paramsFilter; + QVariantMap m_stateFilter; bool m_groupByInterface = false; QString m_sortStateName; + int m_oldCount = 0; + protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const Q_DECL_OVERRIDE; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; diff --git a/libnymea-app/zigbee/zigbeeadapter.cpp b/libnymea-app/zigbee/zigbeeadapter.cpp index 7a4f3741..e1a54552 100644 --- a/libnymea-app/zigbee/zigbeeadapter.cpp +++ b/libnymea-app/zigbee/zigbeeadapter.cpp @@ -122,15 +122,15 @@ bool ZigbeeAdapter::operator==(const ZigbeeAdapter &other) const && m_baudRate == other.baudRate(); } -QDebug operator<<(QDebug debug, const ZigbeeAdapter &adapter) +QDebug operator<<(QDebug dbg, const ZigbeeAdapter &adapter) { - debug.nospace() << "ZigbeeAdapter(" << adapter.name() << " - " << adapter.description(); - debug.nospace() << ", " << adapter.serialPort(); + dbg.nospace() << "ZigbeeAdapter(" << adapter.name() << " - " << adapter.description(); + dbg.nospace() << ", " << adapter.serialPort(); if (adapter.hardwareRecognized()) { - debug.nospace() << " Hardware recognized: " << adapter.backend(); - debug.nospace() << ", " << adapter.baudRate(); + dbg.nospace() << " Hardware recognized: " << adapter.backend(); + dbg.nospace() << ", " << adapter.baudRate(); } - debug.nospace() << ")"; - return debug.space(); + dbg.nospace() << ")"; + return dbg.space(); } diff --git a/nymea-app.pro b/nymea-app.pro index d2930a6f..1c0d16a9 100644 --- a/nymea-app.pro +++ b/nymea-app.pro @@ -3,8 +3,10 @@ TEMPLATE=subdirs include(shared.pri) message("APP_VERSION: $${APP_VERSION} ($${APP_REVISION})") -SUBDIRS = libnymea-app nymea-app -nymea-app.depends = libnymea-app +SUBDIRS = libnymea-app experiences nymea-app + +experiences.depends = libnymea-app +nymea-app.depends = libnymea-app experiences withtests: { SUBDIRS += tests diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index bd532586..4979d230 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -299,5 +299,10 @@ ui/images/plus.svg ui/images/minus.svg ui/images/sensors/vibration.svg + ui/images/calendar.svg + ui/images/sensors/window-closed.svg + ui/images/sensors/window-open.svg + ui/images/infinity.svg + ui/images/edit-paste.svg diff --git a/nymea-app/main.cpp b/nymea-app/main.cpp index c45c0205..7f952cc3 100644 --- a/nymea-app/main.cpp +++ b/nymea-app/main.cpp @@ -38,6 +38,7 @@ #include #include "libnymea-app-core.h" +#include "libnymea-app-airconditioning.h" #include "stylecontroller.h" #include "pushnotifications.h" @@ -129,10 +130,13 @@ int main(int argc, char *argv[]) application.installTranslator(&overlayTranslator); #endif - registerQmlTypes(); + Nymea::Core::registerQmlTypes(); + Nymea::AirConditioning::registerQmlTypes(); QQmlApplicationEngine *engine = new QQmlApplicationEngine(); + engine->addImportPath(application.applicationDirPath() + "/../experiences/"); + QString defaultStyle; if (parser.isSet(defaultStyleOption)) { defaultStyle = parser.value(defaultStyleOption); diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index c732e74e..f3334c03 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -12,16 +12,22 @@ qtHaveModule(webview) { DEFINES += HAVE_WEBVIEW } -INCLUDEPATH += $$top_srcdir/libnymea-app -LIBS += -L$$top_builddir/libnymea-app/ -lnymea-app +INCLUDEPATH += $$top_srcdir/libnymea-app \ + $$top_srcdir/experiences/airconditioning -win32:Debug:LIBS += -L$$top_builddir/libnymea-app/debug -win32:Release:LIBS += -L$$top_builddir/libnymea-app/release +LIBS += -L$$top_builddir/libnymea-app/ -lnymea-app \ + -L$$top_builddir/experiences/airconditioning -lnymea-app-airconditioning + +win32:Debug:LIBS += -L$$top_builddir/libnymea-app/debug \ + -L$$top_builddir/experiences/airconditioning/debug +win32:Release:LIBS += -L$$top_builddir/libnymea-app/release \ + -L$$top_builddir/experiences/airconditioning/release win32:CXX_FLAGS += /w linux:!android:!nozeroconf:LIBS += -lavahi-client -lavahi-common -PRE_TARGETDEPS += ../libnymea-app -linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a + +linux:!android:PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a \ + $$top_builddir/experiences/airconditioning/libnymea-app-airconditioning.a HEADERS += \ configuredhostsmodel.h \ @@ -89,8 +95,11 @@ android { platformintegration/android/platformpermissionsandroid.cpp \ # https://bugreports.qt.io/browse/QTBUG-83165 - LIBS += -L$${top_builddir}/libnymea-app/$${ANDROID_TARGET_ARCH} - PRE_TARGETDEPS += $$top_builddir/libnymea-app/$${ANDROID_TARGET_ARCH}/libnymea-app.a + CORE_LIBS += -L$${top_builddir}/libnymea-app/$${ANDROID_TARGET_ARCH} + AIRCONDITIONING_LIBS += -L$${top_builddir}/experiences/airconditioning/$${ANDROID_TARGET_ARCH} + + LIBS += $${CORE_LIBS} $${AIRCONDITIONING_LIBS} + message("CORE_LIBS: $${CORE_LIBS}") versioninfo.files = ../version.txt versioninfo.path = / diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 964e666a..466abb97 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -23,7 +23,7 @@ ui/components/Graph.qml ui/components/ErrorDialog.qml ui/components/ShutterControls.qml - ui/components/MeaDialog.qml + ui/components/NymeaDialog.qml ui/components/MainPageTabButton.qml ui/components/AutoSizeMenu.qml ui/components/EmptyViewPlaceholder.qml @@ -289,5 +289,25 @@ ui/mainviews/energy/ConsumerStatsPage.qml ui/mainviews/energy/ConsumersPieChartPage.qml ui/connection/ManualConnectionEntry.qml + ui/mainviews/AirConditioningView.qml + ui/mainviews/airconditioning/ZoneView.qml + ui/mainviews/airconditioning/TimeSchedulePage.qml + ui/mainviews/airconditioning/TemperatureScheduleEditor.qml + ui/customviews/SensorView.qml + ui/components/MultiSelectionTabs.qml + ui/mainviews/airconditioning/ZonePage.qml + ui/mainviews/airconditioning/ZonesView.qml + ui/mainviews/airconditioning/ACSettingsPage.qml + ui/components/StateDial.qml + ui/delegates/SensorListDelegate.qml + ui/mainviews/airconditioning/ZoneStatusIcons.qml + ui/mainviews/airconditioning/BigZoneStatusIcons.qml + ui/mainviews/airconditioning/ZoneInfoWrapper.qml + ui/mainviews/airconditioning/ACChartsPage.qml + ui/mainviews/airconditioning/TimeOverrideDialog.qml + ui/mainviews/airconditioning/TooltipDelegate.qml + ui/mainviews/airconditioning/EditZonePage.qml + ui/mainviews/airconditioning/EditZoneThingsPage.qml + ui/mainviews/airconditioning/LegendDelegate.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 77c7d713..369f9754 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -174,6 +174,7 @@ Page { ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter"; minVersion: "2.0" } ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media"; minVersion: "2.0" } ListElement { name: "dashboard"; source: "DashboardView"; displayName: qsTr("Dashboard"); icon: "dashboard"; minVersion: "5.5" } + ListElement { name: "airconditioning"; source: "AirConditioningView"; displayName: qsTr("AC"); icon: "sensors"; minVersion: "6.2" } } ListModel { @@ -254,10 +255,15 @@ Page { readonly property int scrollOffset: swipeView.currentItem ? swipeView.currentItem.item.contentY : 0 readonly property int headerBlurSize: Math.min(headerSize, scrollOffset * 2) + Background { + anchors.fill: parent + } + SwipeView { id: swipeView anchors.fill: parent opacity: d.configOverlay === null ? 1 : 0 + visible: !engine.thingManager.fetchingData Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } Repeater { @@ -820,7 +826,7 @@ Page { Component { id: connectionDialogComponent - MeaDialog { + NymeaDialog { id: connectionDialog title: engine.jsonRpcClient.currentHost.name standardButtons: Dialog.NoButton diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index db63609e..5f6991b4 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -254,6 +254,10 @@ ApplicationWindow { return qsTr("Cleaning robots") case "electricvehicle": return qsTr("Electric cars"); + case "closablesensor": + return qsTr("Door/Window sensors"); + case "o3sensor": + return qsTr("Ozone sensors"); case "uncategorized": return qsTr("Uncategorized") default: @@ -318,7 +322,7 @@ ApplicationWindow { case "presencesensor": return Qt.resolvedUrl("images/sensors/presence.svg") case "closablesensor": - return Qt.resolvedUrl("images/sensors/closable.svg") + return Qt.resolvedUrl("images/sensors/window-closed.svg") case "windspeedsensor": return Qt.resolvedUrl("images/sensors/windspeed.svg") case "watersensor": diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index 9d44bb75..dff40eda 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -20,6 +20,7 @@ Item { property int cornerRadius: 10 property int smallCornerRadius: 6 + readonly property int extraSmallMargins: 4 readonly property int smallMargins: 8 readonly property int margins: 16 readonly property int bigMargins: 32 @@ -89,8 +90,8 @@ Item { // Icon/graph colors for various interfaces property var interfaceColors: { "temperaturesensor": red, - "humiditysensor": blue, - "moisturesensor": lightBlue, + "humiditysensor": lightBlue, + "moisturesensor": blue, "lightsensor": yellow, "conductivitysensor": green, "pressuresensor": gray, @@ -108,7 +109,7 @@ Item { "smartmeterproducer": lime, "energymeter": darkBlue, "heating" : red, - "cooling": lightBlue, + "cooling": blue, "thermostat": blue, "irrigation": blue, "windspeedsensor": blue, diff --git a/nymea-app/ui/appsettings/CloudLoginPage.qml b/nymea-app/ui/appsettings/CloudLoginPage.qml index e1c5adc3..609c3648 100644 --- a/nymea-app/ui/appsettings/CloudLoginPage.qml +++ b/nymea-app/ui/appsettings/CloudLoginPage.qml @@ -490,7 +490,7 @@ SettingsPageBase { popup.open() return; } - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var popup = dialog.createObject(app, {headerIcon: "../images/tick.svg", title: qsTr("Yay!"), text: qsTr("Your password has been reset.")}) popup.accepted.connect(function() { pageStack.pop(root); @@ -556,7 +556,7 @@ SettingsPageBase { Component { id : logoutDialogComponent - MeaDialog { + NymeaDialog { id: logoutDialog title: qsTr("Goodbye") text: qsTr("Sorry to see you go. If you log out you won't be able to connect to %1 systems remotely any more. However, you can come back any time, we'll keep your user account. If you whish to completely delete your account and all the data associated with it, check the box below before hitting ok. If you decide to delete your account, all your personal information will be removed from %1:cloud and cannot be restored.").arg(Configuration.systemName) diff --git a/nymea-app/ui/components/BigThingTile.qml b/nymea-app/ui/components/BigThingTile.qml index b663d174..f43f0ebe 100644 --- a/nymea-app/ui/components/BigThingTile.qml +++ b/nymea-app/ui/components/BigThingTile.qml @@ -23,7 +23,7 @@ BigTile { id: headerRow visible: root.showHeader width: parent.width - Layout.margins: app.margins / 2 + Layout.margins: Style.margins / 2 Label { Layout.fillWidth: true text: root.thing.name diff --git a/nymea-app/ui/components/BrowserContextMenu.qml b/nymea-app/ui/components/BrowserContextMenu.qml index 6a295d1f..643bb31d 100644 --- a/nymea-app/ui/components/BrowserContextMenu.qml +++ b/nymea-app/ui/components/BrowserContextMenu.qml @@ -34,7 +34,7 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../delegates" -MeaDialog { +NymeaDialog { id: root property Thing thing diff --git a/nymea-app/ui/components/ButtonControls.qml b/nymea-app/ui/components/ButtonControls.qml index 143f333b..ab9196ae 100644 --- a/nymea-app/ui/components/ButtonControls.qml +++ b/nymea-app/ui/components/ButtonControls.qml @@ -88,6 +88,7 @@ RowLayout { // return qsTr("%1 installed").arg(thingsProxy.count) } console.warn("InterfaceTile, inlineButtonControl 1: Unhandled interface", model.name) + return "" } font.pixelSize: app.smallFont elide: Text.ElideRight diff --git a/nymea-app/ui/components/CircleBackground.qml b/nymea-app/ui/components/CircleBackground.qml index 2722800d..09088eaa 100644 --- a/nymea-app/ui/components/CircleBackground.qml +++ b/nymea-app/ui/components/CircleBackground.qml @@ -44,11 +44,6 @@ Item { property bool on: false property alias showOnGradient: opacityMask.visible - property bool showProgress: false - property double progressFrom: 0 - property double progressTo: 100 - property double progress: 50 - readonly property Item contentItem: background signal clicked() @@ -78,7 +73,7 @@ Item { ColorIcon { id: icon anchors.centerIn: background - size: Style.hugeIconSize + size: Math.min(Style.hugeIconSize, background.width * 0.4) color: root.on ? root.onColor : Style.iconColor Behavior on color { ColorAnimation { duration: Style.animationDuration } } } @@ -102,9 +97,4 @@ Item { Behavior on opacity { NumberAnimation { duration: Style.animationDuration } } } - Item { - id: contentContainer - anchors.fill: background - } - } diff --git a/nymea-app/ui/components/ConnectionInfoDialog.qml b/nymea-app/ui/components/ConnectionInfoDialog.qml index 8b10686f..edbc3744 100644 --- a/nymea-app/ui/components/ConnectionInfoDialog.qml +++ b/nymea-app/ui/components/ConnectionInfoDialog.qml @@ -162,7 +162,7 @@ Dialog { Component { id: addManualConnectionComponent - MeaDialog { + NymeaDialog { id: addManualConnectionDialog standardButtons: Dialog.Ok | Dialog.Cancel property alias rpcUrl: manualEntry.rpcUrl diff --git a/nymea-app/ui/components/Dial.qml b/nymea-app/ui/components/Dial.qml index 2736776c..c259cefa 100644 --- a/nymea-app/ui/components/Dial.qml +++ b/nymea-app/ui/components/Dial.qml @@ -37,49 +37,51 @@ import "../utils" Item { id: root - property Thing thing: null - property string stateName: "" - property StateType stateType: thing ? thing.thingClass.stateTypes.findByName(stateName) : null + property double minValue: 0 + property double maxValue: 100 + property double precision: 1 + property double value: 50 + property double activeValue: minValue + readonly property alias pendingValue: d.pendingValue property color color: Style.accentColor property bool on: true - property int precision: 1 - - readonly property State progressState: thing ? thing.states.getState(stateType.id) : null - readonly property State powerState: thing ? thing.stateByName("power") : null property int startAngle: 135 property int maxAngle: 270 - readonly property int steps: canvas.roundToPrecision(root.progressState.maxValue - root.progressState.minValue) / root.precision + 1 - readonly property double stepSize: (root.progressState.maxValue - root.progressState.minValue) / steps - readonly property double anglePerStep: maxAngle / steps + readonly property int steps: canvas.roundToPrecision(maxValue - minValue) / root.precision + 1 + readonly property double stepSize: (maxValue - minValue) / (steps - 1) + readonly property double anglePerStep: maxAngle / (steps - 1) + signal pressed() + signal released() + signal clicked() + signal moved(double value) - ActionQueue { - id: actionQueue - thing: root.thing - stateType: root.stateType + QtObject { + id: d + property double pendingValue: root.value onPendingValueChanged: canvas.requestPaint() - } - ActionQueue { - id: powerActionQueue - thing: root.thing - stateName: "power" + property bool pending: false } - - Connections { - target: root.progressState - onValueChanged: { - canvas.requestPaint() + onValueChanged: { + if (d.pending && value == d.pendingValue) { + d.pending = false } } + Binding { + target: d + property: "pendingValue" + value: root.value + when: !d.pending + } Canvas { id: canvas - anchors.centerIn: root - width: Math.min(root.width, root.height) + anchors.centerIn: root + width: Math.min(400, Math.min(root.width, root.height)) height: width property color effectColor: root.on ? root.color : Style.iconColor @@ -98,23 +100,25 @@ Item { var center = { x: canvas.width / 2, y: canvas.height / 2 }; // Step lines - var currentValue = actionQueue.pendingValue || root.progressState.value - var currentStep; - if (root.progressState) { - currentStep = roundToPrecision(currentValue - root.progressState.minValue) / root.precision - } - -// print("* current step", currentStep, root.steps, currentValue) + var currentValue = d.pendingValue + var currentStep = roundToPrecision(currentValue - minValue) / root.precision + var activeStep = roundToPrecision(root.activeValue - minValue) / root.precision for(var step = 0; step < steps; step += root.precision) { var angle = step * anglePerStep + startAngle; var innerRadius = canvas.width * 0.4 var outerRadius = canvas.width * 0.5 - if (step <= currentStep) { + if (step == currentStep) { ctx.strokeStyle = canvas.effectColor innerRadius = canvas.width * 0.38 ctx.lineWidth = 4; + } else if (step < currentStep && step >= activeStep) { + ctx.strokeStyle = canvas.effectColor + ctx.lineWidth = 2; + } else if (step > currentStep && step <= activeStep) { + ctx.strokeStyle = canvas.effectColor + ctx.lineWidth = 2; } else { ctx.strokeStyle = Style.tileOverlayColor; ctx.lineWidth = 1; @@ -142,6 +146,7 @@ Item { MouseArea { anchors.fill: canvas + preventStealing: dragging property bool dragging: false property double lastAngle @@ -150,12 +155,14 @@ Item { onPressed: { angleDiff = 0 lastAngle = calculateAngle(mouseX, mouseY) + root.pressed() } onReleased: { - if (!dragging && root.powerState) { + root.released() + if (!dragging) { PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) - powerActionQueue.sendValue(!root.powerState.value) + root.clicked() } dragging = false } @@ -180,11 +187,13 @@ Item { var valueDiff = angleDiff / root.anglePerStep * root.stepSize valueDiff = canvas.roundToPrecision(valueDiff) if (Math.abs(valueDiff) > 0) { - var currentValue = actionQueue.pendingValue || root.progressState.value + var currentValue = d.pendingValue var newValue = currentValue + valueDiff - newValue = Math.min(root.progressState.maxValue, Math.max(root.progressState.minValue, newValue)) + newValue = Math.min(root.maxValue, Math.max(root.minValue, newValue)) if (currentValue !== newValue) { - actionQueue.sendValue(newValue) + d.pendingValue = newValue; + d.pending = true; + root.moved(newValue) } var steps = Math.round(valueDiff / root.stepSize) angleDiff -= steps * root.anglePerStep diff --git a/nymea-app/ui/components/ErrorDialog.qml b/nymea-app/ui/components/ErrorDialog.qml index 04fb1fcd..9e6cfa47 100644 --- a/nymea-app/ui/components/ErrorDialog.qml +++ b/nymea-app/ui/components/ErrorDialog.qml @@ -32,7 +32,7 @@ import QtQuick 2.8 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.2 -MeaDialog { +NymeaDialog { id: root title: qsTr("Oh snap!") diff --git a/nymea-app/ui/components/Imprint.qml b/nymea-app/ui/components/Imprint.qml index bf694e8c..20e42d9b 100644 --- a/nymea-app/ui/components/Imprint.qml +++ b/nymea-app/ui/components/Imprint.qml @@ -66,7 +66,7 @@ Item { clickCounter++; if (clickCounter >= 10) { settings.showHiddenOptions = !settings.showHiddenOptions - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = settings.showHiddenOptions ? qsTr("Developer options are now enabled. If you have found this by accident, it is most likely not of any use for you. It will just enable some nerdy developer gibberish in the app. Tap the icon another 10 times to disable it again.") : qsTr("Developer options are now disabled.") diff --git a/nymea-app/ui/components/MainViewBase.qml b/nymea-app/ui/components/MainViewBase.qml index 2f3a843e..3819a901 100644 --- a/nymea-app/ui/components/MainViewBase.qml +++ b/nymea-app/ui/components/MainViewBase.qml @@ -54,10 +54,6 @@ Item { print("handleEvent not implemented in", title) } - Background { - anchors.fill: parent - } - // Prevent scroll events to swipe left/right in case they fall through the grid MouseArea { anchors.fill: parent diff --git a/nymea-app/ui/components/MediaPlayer.qml b/nymea-app/ui/components/MediaPlayer.qml index e76293b8..3b05d392 100644 --- a/nymea-app/ui/components/MediaPlayer.qml +++ b/nymea-app/ui/components/MediaPlayer.qml @@ -477,7 +477,7 @@ Item { Component { id: inputSourceSelectDialogComponent - MeaDialog { + NymeaDialog { id: inputSourceSelectDialog headerIcon: "../images/state-in.svg" title: qsTr("Select input") @@ -505,7 +505,7 @@ Item { Component { id: equalizerComponent - MeaDialog { + NymeaDialog { id: equalizer headerIcon: "../images/media/equalizer.svg" title: qsTr("Equalizer preset") @@ -531,7 +531,7 @@ Item { } Component { id: ambeoModeDialogComponent - MeaDialog { + NymeaDialog { id: ambeoModeDialog standardButtons: Dialog.NoButton ColorIcon { diff --git a/nymea-app/ui/components/MeaDialog.qml b/nymea-app/ui/components/NymeaDialog.qml similarity index 100% rename from nymea-app/ui/components/MeaDialog.qml rename to nymea-app/ui/components/NymeaDialog.qml diff --git a/nymea-app/ui/components/NymeaSpinBox.qml b/nymea-app/ui/components/NymeaSpinBox.qml index 77d12938..ad7f7f7c 100644 --- a/nymea-app/ui/components/NymeaSpinBox.qml +++ b/nymea-app/ui/components/NymeaSpinBox.qml @@ -28,7 +28,7 @@ RowLayout { tmp = parseInt(root.value) } if (tmp != NaN){ - root.value = tmp - 1 + root.value = Math.max(root.from, tmp - 1) root.valueModified(root.value) } } @@ -71,7 +71,7 @@ RowLayout { tmp = parseInt(root.value) } if (tmp != NaN){ - root.value = tmp + 1 + root.value = Math.min(root.to, tmp + 1) root.valueModified(root.value) } } diff --git a/nymea-app/ui/components/StateDial.qml b/nymea-app/ui/components/StateDial.qml new file mode 100644 index 00000000..c241c836 --- /dev/null +++ b/nymea-app/ui/components/StateDial.qml @@ -0,0 +1,211 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU 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 General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import Nymea 1.0 +import QtQuick.Layouts 1.2 +import "../utils" + +Item { + id: root + + property Thing thing: null + property string stateName: "" + property StateType stateType: thing ? thing.thingClass.stateTypes.findByName(stateName) : null + + property color color: Style.accentColor + property bool on: true + property int precision: 1 + + readonly property State progressState: thing ? thing.states.getState(stateType.id) : null + readonly property State powerState: thing ? thing.stateByName("power") : null + + property int startAngle: 135 + property int maxAngle: 270 + readonly property int steps: canvas.roundToPrecision(root.progressState.maxValue - root.progressState.minValue) / root.precision + 1 + readonly property double stepSize: (root.progressState.maxValue - root.progressState.minValue) / steps + readonly property double anglePerStep: maxAngle / steps + + + ActionQueue { + id: actionQueue + thing: root.thing + stateType: root.stateType + onPendingValueChanged: canvas.requestPaint() + } + + ActionQueue { + id: powerActionQueue + thing: root.thing + stateName: "power" + } + + Connections { + target: root.progressState + onValueChanged: { + canvas.requestPaint() + } + } + + + Canvas { + id: canvas + anchors.centerIn: root + width: Math.min(root.width, root.height) + height: width + + property color effectColor: root.on ? root.color : Style.iconColor + Behavior on effectColor { ColorAnimation { duration: Style.animationDuration } } + onEffectColorChanged: requestPaint() + + function roundToPrecision(value) { + var tmp = Math.round(value / root.precision) * root.precision; + return tmp; + } + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + var center = { x: canvas.width / 2, y: canvas.height / 2 }; + + // Step lines + var currentValue = actionQueue.pendingValue || root.progressState.value + var currentStep; + if (root.progressState) { + currentStep = roundToPrecision(currentValue - root.progressState.minValue) / root.precision + } + +// print("* current step", currentStep, root.steps, currentValue) + + for(var step = 0; step < steps; step += root.precision) { + var angle = step * anglePerStep + startAngle; + var innerRadius = canvas.width * 0.4 + var outerRadius = canvas.width * 0.5 + + if (step <= currentStep) { + ctx.strokeStyle = canvas.effectColor + innerRadius = canvas.width * 0.38 + ctx.lineWidth = 4; + } else { + ctx.strokeStyle = Style.tileOverlayColor; + ctx.lineWidth = 1; + } + + ctx.beginPath(); + // rotate + //convert to radians + var rad = angle * Math.PI/180; + var c = Math.cos(rad); + var s = Math.sin(rad); + var innerPointX = center.x + (innerRadius * c); + var innerPointY = center.y + (innerRadius * s); + var outerPointX = center.x + (outerRadius * c); + var outerPointY = center.x + (outerRadius * s); + + ctx.moveTo(innerPointX, innerPointY); + ctx.lineTo(outerPointX, outerPointY); + ctx.stroke(); + ctx.closePath(); + } + } + } + + + MouseArea { + anchors.fill: canvas + + property bool dragging: false + property double lastAngle + property double angleDiff: 0 + + onPressed: { + angleDiff = 0 + lastAngle = calculateAngle(mouseX, mouseY) + } + + onReleased: { + if (!dragging && root.powerState) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + powerActionQueue.sendValue(!root.powerState.value) + } + dragging = false + } + + onPositionChanged: { + var angle = calculateAngle(mouseX, mouseY) + var tmpDiff = angle - lastAngle + if (tmpDiff > 300) { + tmpDiff -= 360 + } + if (tmpDiff < -300) { + tmpDiff += 360 + } + + lastAngle = angle; + + angleDiff += tmpDiff + if (Math.abs(angleDiff) > 1) { + dragging = true + } + + var valueDiff = angleDiff / root.anglePerStep * root.stepSize + valueDiff = canvas.roundToPrecision(valueDiff) + if (Math.abs(valueDiff) > 0) { + var currentValue = actionQueue.pendingValue || root.progressState.value + var newValue = currentValue + valueDiff + newValue = Math.min(root.progressState.maxValue, Math.max(root.progressState.minValue, newValue)) + print("newValue", newValue) + if (currentValue !== newValue) { + actionQueue.sendValue(newValue) + } + var steps = Math.round(valueDiff / root.stepSize) + angleDiff -= steps * root.anglePerStep + } + } + + function calculateAngle(mouseX, mouseY) { + // transform coords to center of dial + mouseX -= canvas.width / 2 + mouseY -= canvas.height / 2 + + var rad = Math.atan(mouseY / mouseX); + var angle = rad * 180 / Math.PI + + angle += 90; + + if (mouseX < 0 && mouseY >= 0) angle = 180 + angle; + if (mouseX < 0 && mouseY < 0) angle = 180 + angle; + + return angle; + } + } +} diff --git a/nymea-app/ui/components/ThingContextMenu.qml b/nymea-app/ui/components/ThingContextMenu.qml index 85601652..433023eb 100644 --- a/nymea-app/ui/components/ThingContextMenu.qml +++ b/nymea-app/ui/components/ThingContextMenu.qml @@ -99,7 +99,7 @@ AutoSizeMenu { Component { id: addToGroupDialog - MeaDialog { + NymeaDialog { title: qsTr("Groups for %1").arg(root.thing.name) headerIcon: "../images/groups.svg" // NOTE: If CloseOnPressOutside is active (default) it will break the QtVirtualKeyboard diff --git a/nymea-app/ui/components/ThingStatusIcons.qml b/nymea-app/ui/components/ThingStatusIcons.qml index a2837e88..a702ae9f 100644 --- a/nymea-app/ui/components/ThingStatusIcons.qml +++ b/nymea-app/ui/components/ThingStatusIcons.qml @@ -28,7 +28,7 @@ RowLayout { anchors.fill: parent anchors.margins: -app.margins / 4 onClicked: { - var dialogComponent = Qt.createComponent("MeaDialog.qml") + var dialogComponent = Qt.createComponent("NymeaDialog.qml") var currentVersionState = root.thing.stateByName("currentVersion") var availableVersionState = root.thing.stateByName("availableVersion") var text = qsTr("An update for %1 is available. Do you want to start the update now?").arg(root.thing.name) diff --git a/nymea-app/ui/connection/CertificateErrorDialog.qml b/nymea-app/ui/connection/CertificateErrorDialog.qml index bf54212a..8ea06180 100644 --- a/nymea-app/ui/connection/CertificateErrorDialog.qml +++ b/nymea-app/ui/connection/CertificateErrorDialog.qml @@ -35,7 +35,7 @@ import QtQuick.Layouts 1.3 import Nymea 1.0 import "../components" -MeaDialog { +NymeaDialog { id: root title: qsTr("Insecure connection") diff --git a/nymea-app/ui/customviews/SensorView.qml b/nymea-app/ui/customviews/SensorView.qml new file mode 100644 index 00000000..168e6cf8 --- /dev/null +++ b/nymea-app/ui/customviews/SensorView.qml @@ -0,0 +1,438 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 +import Nymea 1.0 +import NymeaApp.Utils 1.0 +import "qrc:/ui/components" +import QtGraphicalEffects 1.0 + +Item { + id: root + + property Thing thing: null + property string interfaceName: "" + + property var interfaceStateMap: { + "temperaturesensor": "temperature", + "humiditysensor": "humidity", + "pressuresensor": "pressure", + "moisturesensor": "moisture", + "lightsensor": "lightIntensity", + "conductivitysensor": "conductivity", + "noisesensor": "noise", + "cosensor": "co", + "co2sensor": "co2", + "gassensor": "gasLevel", + "presencesensor": "isPresent", + "daylightsensor": "daylight", + "closablesensor": "closed", + "watersensor": "waterDetected", + "firesensor": "fireDetected", + "waterlevelsensor": "waterLevel", + "phsensor": "ph", + "o2sensor": "o2saturation", + "o3sensor": "o3", + "orpsensor": "orp", + "vocsensor": "voc", + "cosensor": "co", + "pm10sensor": "pm10", + "pm25sensor": "pm25", + "no2sensor": "no2" + } + + CircleBackground { + id: background + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) - Style.margins + height: width + + readonly property StateType sensorStateType: root.thing ? root.thing.thingClass.stateTypes.findByName(interfaceStateMap[root.interfaceName]) : null + readonly property State sensorState: root.thing ? root.thing.stateByName(interfaceStateMap[interfaceName]) : null + + onColor: { + if (root.interfaceName == "closablesensor") { + return sensorState.value === true ? Style.green : Style.red + } + return app.interfaceToColor(root.interfaceName) + } + on: { + if (root.interfaceName == "closablesensor") { + return true + } + return sensorStateType && sensorStateType.type.toLowerCase() == "bool" && sensorState.value === true + } + iconSource: { + if (root.interfaceName == "closablesensor") { + return sensorState.value === true ? "sensors/window-closed" : "sensors/window-open" + } + var map = [ + "presencesensor", + "daylightsensor", + "firesensor", + "watersensor" + ] + return map.indexOf(root.interfaceName) >= 0 ? app.interfaceToIcon(root.interfaceName) : "" + } + + Loader { + anchors.centerIn: parent + width: background.contentItem.width + height: background.contentItem.height + property StateType stateType: root.thing.thingClass.stateTypes.findByName(interfaceStateMap[root.interfaceName]) + property State state: root.thing.stateByName(interfaceStateMap[root.interfaceName]) + property string interfaceName: root.interfaceName + property var minValue: { + if (["temperaturesensor"].indexOf(root.interfaceName) >= 0) { + return Types.toUiValue(-50, Types.UnitDegreeCelsius) + } + if (["pressuresensor"].indexOf(root.interfaceName) >= 0) { + return Types.toUiValue(state.minValue, Types.UnitMilliBar) + } + + return state.minValue + } + property var maxValue: { + if (["temperaturesensor"].indexOf(root.interfaceName) >= 0) { + return Types.toUiValue(50, Types.UnitDegreeCelsius) + } + if (["pressuresensor"].indexOf(root.interfaceName) >= 0) { + return Types.toUiValue(state.maxValue, Types.UnitMilliBar) + } + return state.maxValue + } + + sourceComponent: { + if (stateType.type.toLowerCase() == "bool") { + return boolComponent; + } + + var progressInterfaces = [ + "humiditysensor", + "o2sensor", + "temperaturesensor", + "moisturesensor", + "conductivitysensor", + "gassensor", + "lightsensor", + "orpsensor", + "co2sensor", + "phsensor", + "pressuresensor", + "waterlevelsensor", + "windspeedsensor" + ] + if (progressInterfaces.indexOf(root.interfaceName) >= 0) { + return progressComponent + } + var scaleInterfaces = [ + "vocsensor", + "cosensor", + "o3sensor", + "pm10sensor", + "pm25sensor", + "no2sensor" + ] + if (scaleInterfaces.indexOf(root.interfaceName) >= 0) { + return scaleComponent + } + } + } + } + + Component { + id: boolComponent + Rectangle { + property State state: parent.state + + radius: width / 2 + color: "transparent" + border.color: { + if (root.interfaceName == "closablesensor") { + return state.value === true ? Style.green : Style.red + } + return app.interfaceToColor(root.interfaceName) + } + border.width: width * .1 + visible: { + if (root.interfaceName == "closablesensor") { + return true + } + + return state.value === true + } + } + } + + Component { + id: progressComponent + Canvas { + id: progressCanvas + property string interfaceName: parent.interfaceName + property StateType stateType: parent.stateType + property State state: parent.state + property var minValue: parent.minValue + property var maxValue: parent.maxValue + + property double progress: (progressCanvas.state.value - progressCanvas.minValue) / (progressCanvas.maxValue - progressCanvas.minValue) + Behavior on progress { NumberAnimation { duration: Style.slowAnimationDuration; easing.type: Easing.InOutQuad } } + onProgressChanged: requestPaint(); + + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: -Style.smallMargins + width: parent.width * 0.6 + + Label { + Layout.fillWidth: true + text: Types.toUiValue(progressCanvas.state.value, progressCanvas.stateType.unit).toFixed(1) + wrapMode: Text.WordWrap + font.pixelSize: Math.min(Style.hugeFont.pixelSize, progressCanvas.height / 8) + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + Label { + Layout.fillWidth: true + text: Types.toUiUnit(progressCanvas.stateType.unit) + font.pixelSize: Math.min(Style.largeFont.pixelSize, progressCanvas.height / 12) + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + ctx.beginPath() + ctx.fillStyle = Style.foregroundColor + + ctx.translate(width / 2, height / 2) + ctx.rotate(135 * Math.PI / 180) + + ctx.lineCap = "round" + ctx.lineWidth = width * .1 + + ctx.beginPath() + ctx.strokeStyle = Style.tileOverlayColor + var startAngle = 0 + var endAngle = 270 + var radStart = startAngle * Math.PI/180; + var radEnd = endAngle * Math.PI/180; + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) + ctx.stroke() + ctx.closePath() + + ctx.beginPath() + ctx.strokeStyle = app.interfaceToColor(progressCanvas.interfaceName) + radEnd *= progressCanvas.progress + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) + ctx.stroke() + ctx.closePath() + } + + ColorIcon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.smallMargins + name: app.interfaceToIcon(progressCanvas.interfaceName) + size: Math.min(Style.bigIconSize, parent.height / 5) + color: app.interfaceToColor(progressCanvas.interfaceName) + } + } + } + + + Component { + id: scaleComponent + Item { + id: scaleCanvas + property string interfaceName: parent.interfaceName + property StateType stateType: parent.stateType + property State state: parent.state + property var minValue: parent.minValue + property var maxValue: parent.maxValue + + property int scaleWidth: width * .1 + + property var scale: { + switch (interfaceName) { + case "vocsensor": + return AirQualityIndex.iaqVoc + case "cosensor": + return AirQualityIndex.caqiCo + case "o3sensor": + return AirQualityIndex.caqiO3 + case "pm10sensor": + return AirQualityIndex.caqiPm10 + case "pm25sensor": + return AirQualityIndex.caqiPm25 + case "no2sensor": + return AirQualityIndex.caqiNo2 + } + return baseScale + } + property var baseScale: [ + { + "value": maxValue, + "angle": 270, + "color": Style.tileOverlayColor + } + ] + + property var currentIndex: { + for (var i = 0; i < scale.length; i++) { + if (state.value <= scale[i].value) { + return i; + } + } + log.warn("Value out of scale!") + return -1 + } + + property double angle: { + var baseAngle = 0 + var baseValue = 0; + if (currentIndex > 0) { + baseAngle = scale[currentIndex-1].angle + baseValue = scale[currentIndex-1].value + } + var valueRange = scale[currentIndex].value - baseValue + var angleRange = scale[currentIndex].angle - baseAngle + var progress = (state.value - baseValue) / (scale[currentIndex].value - baseValue) + return baseAngle + angleRange * progress + } + Behavior on angle { NumberAnimation { duration: Style.slowAnimationDuration; easing.type: Easing.InOutQuad } } + onAngleChanged: maskCanvas.requestPaint(); + + Canvas { + id: baseCanvas + anchors.fill: parent + visible: false + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + ctx.beginPath() + ctx.fillStyle = Style.foregroundColor + + ctx.translate(width / 2, height / 2) + ctx.rotate(135 * Math.PI / 180) + + ctx.lineCap = "round" + ctx.lineWidth = scaleCanvas.scaleWidth + + // paint first rounded + ctx.beginPath() + ctx.strokeStyle = scaleCanvas.scale[0].color + var startAngle = 0 + var endAngle = scaleCanvas.scale[0].angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) + ctx.stroke() + ctx.closePath() + + // paint last rounded + ctx.beginPath() + ctx.strokeStyle = scaleCanvas.scale[scaleCanvas.scale.length - 1].color + startAngle = scaleCanvas.scale[scaleCanvas.scale.length - 2].angle * Math.PI/180 + endAngle = scaleCanvas.scale[scaleCanvas.scale.length - 1].angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) + ctx.stroke() + ctx.closePath() + + // paint inner parts + ctx.lineCap = "butt" + for (var i = 1; i < scaleCanvas.scale.length - 1; i++) { + ctx.beginPath() + ctx.strokeStyle = scaleCanvas.scale[i].color + startAngle = scaleCanvas.scale[i - 1].angle * Math.PI/180 + endAngle = scaleCanvas.scale[i].angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) + ctx.stroke() + ctx.closePath() + } + } + } + Canvas { + id: maskCanvas + anchors.fill: parent + visible: false + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + ctx.beginPath() + ctx.fillStyle = Style.foregroundColor + + ctx.translate(width / 2, height / 2) + ctx.rotate(135 * Math.PI / 180) + + ctx.lineCap = "round" + ctx.lineWidth = width * .1 + + ctx.beginPath() + ctx.strokeStyle = "#55ffffff" + var startAngle = 0 + var endAngle = 270 + var radStart = startAngle * Math.PI/180; + var radEnd = endAngle * Math.PI/180; + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) + ctx.stroke() + ctx.closePath() + + ctx.beginPath() + ctx.strokeStyle = "#000000" + radEnd = scaleCanvas.angle * Math.PI/180 + ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) + ctx.stroke() + ctx.closePath() + } + } + + OpacityMask { + anchors.fill: parent + source: baseCanvas + maskSource: maskCanvas + } + + + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: -Style.smallMargins + width: parent.width * 0.6 + + Label { + Layout.fillWidth: true + text: scaleCanvas.scale[scaleCanvas.currentIndex].text + font.pixelSize: Math.min(Style.hugeFont.pixelSize, scaleCanvas.height / 8) + wrapMode: Text.WordWrap + // color: scaleCanvas.scale[scaleCanvas.currentIndex].color + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 2 + } + Label { + Layout.fillWidth: true + text: Types.toUiValue(scaleCanvas.state.value, scaleCanvas.stateType.unit).toFixed(1) + " " + Types.toUiUnit(scaleCanvas.stateType.unit) + font.pixelSize: Math.min(Style.largeFont.pixelSize, scaleCanvas.height / 12) + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + + + ColorIcon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.smallMargins + // anchors.verticalCenterOffset: scaleCanvas.height / 2 - height + name: app.interfaceToIcon(scaleCanvas.interfaceName) + size: Math.min(Style.bigIconSize, parent.height / 5) + color: app.interfaceToColor(scaleCanvas.interfaceName) + } + } + } + +} diff --git a/nymea-app/ui/customviews/ThermostatController.qml b/nymea-app/ui/customviews/ThermostatController.qml index 15b34f2e..e8ec6783 100644 --- a/nymea-app/ui/customviews/ThermostatController.qml +++ b/nymea-app/ui/customviews/ThermostatController.qml @@ -1,8 +1,11 @@ import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 import Nymea 1.0 import "../utils" import "../components" + Item { id: root @@ -44,6 +47,21 @@ Item { property double stepSize: (root.targetTemperatureState.maxValue - root.targetTemperatureState.minValue) / steps property double anglePerStep: maxAngle / steps + readonly property double currentValue: actionQueue.pendingValue || root.targetTemperatureState.value + readonly property double targetTempStep: roundToPrecision(currentValue - root.targetTemperatureState.minValue) * (1/root.precision) + readonly property double currentTempStep: root.temperatureState ? roundToPrecision(root.temperatureState.value - root.targetTemperatureState.minValue) * (1/root.precision) : 0 + + + readonly property double targetTemperature: roundToPrecision(Types.toUiValue(currentValue, root.targetTemperatureStateType.unit)) + + readonly property color currentColor: { + if (currentTempStep && currentTempStep < targetTempStep) { + return app.interfaceToColor("heating"); + } else if (currentTempStep && currentTempStep > targetTempStep) { + return app.interfaceToColor("cooling"); + } + return Style.accentColor; + } function angleToValue(angle) { var from = root.targetTemperatureState.minValue @@ -51,6 +69,30 @@ Item { return (to - from) * angle / maxAngle + from } + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: -Style.smallMargins + width: parent.width * 0.6 + + Label { + Layout.fillWidth: true + text: canvas.targetTemperature.toFixed(1) + Types.toUiUnit(Types.UnitDegreeCelsius) + wrapMode: Text.WordWrap + font.pixelSize: Math.min(Style.hugeFont.pixelSize, canvas.height / 8) + maximumLineCount: 2 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + color: canvas.currentColor + } + Label { + Layout.fillWidth: true + text: Types.toUiValue(root.temperatureState.value, root.temperatureStateType.unit).toFixed(1) + Types.toUiUnit(Types.UnitDegreeCelsius) + font.pixelSize: Math.min(Style.largeFont.pixelSize, canvas.height / 12) + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + onPaint: { var ctx = canvas.getContext('2d'); ctx.save(); @@ -76,39 +118,33 @@ Item { ctx.closePath(); // Step lines - var currentValue = actionQueue.pendingValue || root.targetTemperatureState.value - var targetTempStep = roundToPrecision(currentValue - root.targetTemperatureState.minValue) * (1/root.precision) - var currentTempStep; - if (root.temperatureState) { - currentTempStep = roundToPrecision(root.temperatureState.value - root.targetTemperatureState.minValue) * (1/root.precision) - } for(var step = 0; step < steps; step += root.precision) { var angle = step * anglePerStep + startAngle; var innerRadius = canvas.width * 0.4 var outerRadius = canvas.width * 0.5 - if (targetTempStep === step) { - if (currentTempStep && currentTempStep < targetTempStep) { + if (canvas.targetTempStep === step) { + if (canvas.currentTempStep && canvas.currentTempStep < canvas.targetTempStep) { ctx.strokeStyle = app.interfaceToColor("heating"); - } else if (currentTempStep && currentTempStep > targetTempStep) { + } else if (canvas.currentTempStep && canvas.currentTempStep > canvas.targetTempStep) { ctx.strokeStyle = app.interfaceToColor("cooling"); } else { ctx.strokeStyle = Style.accentColor; } innerRadius = canvas.width * 0.38 ctx.lineWidth = 4; - } else if (currentTempStep && currentTempStep === step) { - if (currentTempStep < targetTempStep) { + } else if (canvas.currentTempStep && canvas.currentTempStep === step) { + if (canvas.currentTempStep < canvas.targetTempStep) { ctx.strokeStyle = app.interfaceToColor("heating"); } else { ctx.strokeStyle = app.interfaceToColor("cooling"); } ctx.lineWidth = 3; - } else if (currentTempStep && currentTempStep < step && step < targetTempStep) { + } else if (canvas.currentTempStep && canvas.currentTempStep < step && step < canvas.targetTempStep) { ctx.strokeStyle = app.interfaceToColor("heating"); ctx.lineWidth = 2; - } else if (currentTempStep && currentTempStep > step && step > targetTempStep) { + } else if (canvas.currentTempStep && canvas.currentTempStep > step && step > canvas.targetTempStep) { ctx.strokeStyle = app.interfaceToColor("cooling"); ctx.lineWidth = 2; } else { @@ -132,28 +168,6 @@ Item { ctx.stroke(); ctx.closePath(); } - - ctx.beginPath(); - ctx.font = "" + Style.hugeFont.pixelSize + "px " + Style.fontFamily; - ctx.fillStyle = Style.foregroundColor; - var roundedTargetTemp = Types.toUiValue(currentValue, root.targetTemperatureStateType.unit) - roundedTargetTemp = roundToPrecision(roundedTargetTemp).toFixed(1) + "°" - var size = ctx.measureText(roundedTargetTemp) - ctx.text(roundedTargetTemp, center.x - size.width / 2, center.y + Style.hugeFont.pixelSize / 2); - ctx.fill(); - ctx.closePath(); - - if (root.temperatureState) { - ctx.beginPath(); - ctx.font = "" + Style.bigFont.pixelSize + "px " + Style.fontFamily; - var roundedTemp = Types.toUiValue(root.temperatureState.value, root.temperatureStateType.unit) - roundedTemp = roundToPrecision(roundedTemp) + "°" - size = ctx.measureText(roundedTemp) - ctx.text(roundedTemp, center.x - size.width / 2, center.y + Style.hugeFont.pixelSize + Style.margins); - ctx.fill(); - ctx.closePath(); - } - ctx.restore(); } @@ -163,10 +177,12 @@ Item { } } + ColorIcon { - width: Style.largeIconSize - height: width - anchors { bottom: canvas.bottom; horizontalCenter: canvas.horizontalCenter; margins: Style.margins } + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.smallMargins + size: Math.min(Style.bigIconSize, parent.height / 5) name: root.heatingOnState && root.heatingOnState.value === true ? "../images/thermostat/heating.svg" : root.coolingOnState && root.coolingOnState.value === true diff --git a/nymea-app/ui/delegates/InterfaceTile.qml b/nymea-app/ui/delegates/InterfaceTile.qml index 8894c3da..d7086798 100644 --- a/nymea-app/ui/delegates/InterfaceTile.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -76,6 +76,7 @@ MainPageTile { switch (iface.name) { case "heating": case "cooling": + case "thermostat": case "sensor": page = "SensorsDeviceListPage.qml" break; diff --git a/nymea-app/ui/delegates/ParamDelegate.qml b/nymea-app/ui/delegates/ParamDelegate.qml index b6dd0a6d..cdad3091 100644 --- a/nymea-app/ui/delegates/ParamDelegate.qml +++ b/nymea-app/ui/delegates/ParamDelegate.qml @@ -231,6 +231,7 @@ ItemDelegate { Component.onCompleted: { print("from:", from, "min", root.paramType.minValue) + print("to:", to, "max", root.paramType.maxValue) if (root.value === undefined) { root.value = value } diff --git a/nymea-app/ui/delegates/SensorListDelegate.qml b/nymea-app/ui/delegates/SensorListDelegate.qml new file mode 100644 index 00000000..d3f44fa6 --- /dev/null +++ b/nymea-app/ui/delegates/SensorListDelegate.qml @@ -0,0 +1,167 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU 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 General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" + +BigThingTile { + id: itemDelegate + + contentItem: GridLayout { + id: dataGrid + columns: Math.floor(contentItem.width / 120) + + Repeater { + model: ListModel { + ListElement { interfaceName: "temperaturesensor"; stateName: "temperature" } + ListElement { interfaceName: "humiditysensor"; stateName: "humidity" } + ListElement { interfaceName: "moisturesensor"; stateName: "moisture" } + ListElement { interfaceName: "pressuresensor"; stateName: "pressure" } + ListElement { interfaceName: "lightsensor"; stateName: "lightIntensity" } + ListElement { interfaceName: "conductivitysensor"; stateName: "conductivity" } + ListElement { interfaceName: "noisesensor"; stateName: "noise" } + ListElement { interfaceName: "cosensor"; stateName: "co" } + ListElement { interfaceName: "co2sensor"; stateName: "co2" } + ListElement { interfaceName: "gassensor"; stateName: "gasLevel" } + ListElement { interfaceName: "daylightsensor"; stateName: "daylight" } + ListElement { interfaceName: "presencesensor"; stateName: "isPresent" } + ListElement { interfaceName: "vibrationsensor"; stateName: ""; eventName: "vibrationDetected" } + ListElement { interfaceName: "closablesensor"; stateName: "closed" } + ListElement { interfaceName: "heating"; stateName: "power" } + ListElement { interfaceName: "thermostat"; stateName: "targetTemperature" } + ListElement { interfaceName: "watersensor"; stateName: "waterDetected" } + ListElement { interfaceName: "waterlevelsensor"; stateName: "waterLevel" } + ListElement { interfaceName: "firesensor"; stateName: "fireDetected" } + ListElement { interfaceName: "o2sensor"; stateName: "o2saturation" } + ListElement { interfaceName: "phsensor"; stateName: "ph" } + ListElement { interfaceName: "orpsensor"; stateName: "orp" } + ListElement { interfaceName: "vocsensor"; stateName: "voc" } + ListElement { interfaceName: "pm10sensor"; stateName: "pm10" } + ListElement { interfaceName: "pm25sensor"; stateName: "pm25" } + ListElement { interfaceName: "no2sensor"; stateName: "no2" } + ListElement { interfaceName: "o3sensor"; stateName: "o3" } + } + + delegate: RowLayout { + id: sensorValueDelegate + visible: itemDelegate.thing.thingClass.interfaces.indexOf(model.interfaceName) >= 0 + Layout.preferredWidth: contentItem.width / dataGrid.columns + + property StateType stateType: itemDelegate.thing.thingClass.stateTypes.findByName(model.stateName) + property State stateValue: stateType ? itemDelegate.thing.states.getState(stateType.id) : null + + property EventType eventType: itemDelegate.thing.thingClass.eventTypes.findByName(model.eventName) + LogsModel { + id: eventLogsModel + engine: sensorValueDelegate.eventType != null ? _engine : null + thingId: itemDelegate.thing.id + typeIds: sensorValueDelegate.eventType != null ? [sensorValueDelegate.eventType.id] : [] + live: true + fetchBlockSize: 1 + } + + ColorIcon { + Layout.preferredHeight: Style.iconSize + Layout.preferredWidth: height + Layout.alignment: Qt.AlignVCenter + color: { + switch (model.interfaceName) { + case "closablesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Style.green : Style.red; + case "firesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Style.red : Style.iconColor; + default: + return app.interfaceToColor(model.interfaceName) + } + } + name: { + switch (model.interfaceName) { + case "closablesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Qt.resolvedUrl("qrc:/ui/images/sensors/window-closed.svg") : Qt.resolvedUrl("qrc:/ui/images/sensors/window-open.svg"); + default: + return app.interfacesToIcon([model.interfaceName, "sensor"]) + } + } + } + + Label { + Layout.fillWidth: true + property var unit: sensorValueDelegate.stateType ? sensorValueDelegate.stateType.unit : Types.UnitNone + text: { + switch (model.interfaceName) { + case "closablesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Closed") : qsTr("Open"); + case "presencesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Presence") : qsTr("Vacant"); + case "daylightsensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Daytime") : qsTr("Nighttime"); + case "watersensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Wet") : qsTr("Dry"); + case "firesensor": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Fire") : qsTr("No fire"); + case "heating": + return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("On") : qsTr("Off"); + case "vibrationsensor": { + if (eventLogsModel.count > 0) { + return qsTr("Last vibration: %1").arg(eventLogsModel.get(0).timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)) + } else { + return qsTr("Not moved yet") + } + } + default: + return sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" + ? sensorValueDelegate.stateType.displayName + : sensorValueDelegate.stateValue + ? "%1 %2".arg(Math.round(Types.toUiValue(sensorValueDelegate.stateValue.value, unit) * 100) / 100).arg(Types.toUiUnit(unit)) + : "" + } + } + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: app.smallFont + } + Led { + id: led + visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" && ["presencesensor", "daylightsensor", "heating", "closablesensor", "watersensor", "firesensor"].indexOf(model.interfaceName) < 0 + state: visible && sensorValueDelegate.stateValue.value === true ? "on" : "off" + } + Item { + Layout.preferredWidth: led.width + visible: led.visible + } + + } + } + } +} diff --git a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml index 0dc4070b..993c0bf1 100644 --- a/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/SensorsDeviceListPage.qml @@ -33,13 +33,18 @@ import QtQuick.Controls 2.1 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.1 import Nymea 1.0 -import "../components" +import "qrc:/ui/components" +import "qrc:/ui/delegates" ThingsListPageBase { id: root header: NymeaHeader { - text: root.shownInterfaces.indexOf("heating") >= 0 ? qsTr("Heating") : qsTr("Sensors") + text: root.shownInterfaces.indexOf("heating") >= 0 + ? qsTr("Heating") + : root.shownInterfaces.indexOf("thermostat") >= 0 + ? qsTr("Thermostats") + : qsTr("Sensors") onBackPressed: pageStack.pop() } @@ -60,7 +65,7 @@ ThingsListPageBase { Repeater { model: root.thingsProxy - delegate: BigThingTile { + delegate: SensorListDelegate { id: itemDelegate Layout.preferredWidth: contentGrid.width / contentGrid.columns thing: root.thingsProxy.getThing(model.id) @@ -74,133 +79,6 @@ ThingsListPageBase { enterPage(index) } } - - contentItem: GridLayout { - id: dataGrid - columns: Math.floor(contentItem.width / 120) - - Repeater { - model: ListModel { - ListElement { interfaceName: "temperaturesensor"; stateName: "temperature" } - ListElement { interfaceName: "humiditysensor"; stateName: "humidity" } - ListElement { interfaceName: "moisturesensor"; stateName: "moisture" } - ListElement { interfaceName: "pressuresensor"; stateName: "pressure" } - ListElement { interfaceName: "lightsensor"; stateName: "lightIntensity" } - ListElement { interfaceName: "conductivitysensor"; stateName: "conductivity" } - ListElement { interfaceName: "noisesensor"; stateName: "noise" } - ListElement { interfaceName: "cosensor"; stateName: "co" } - ListElement { interfaceName: "co2sensor"; stateName: "co2" } - ListElement { interfaceName: "gassensor"; stateName: "gasLevel" } - ListElement { interfaceName: "daylightsensor"; stateName: "daylight" } - ListElement { interfaceName: "presencesensor"; stateName: "isPresent" } - ListElement { interfaceName: "vibrationsensor"; stateName: ""; eventName: "vibrationDetected" } - ListElement { interfaceName: "closablesensor"; stateName: "closed" } - ListElement { interfaceName: "heating"; stateName: "power" } - ListElement { interfaceName: "thermostat"; stateName: "targetTemperature" } - ListElement { interfaceName: "watersensor"; stateName: "waterDetected" } - ListElement { interfaceName: "waterlevelsensor"; stateName: "waterLevel" } - ListElement { interfaceName: "firesensor"; stateName: "fireDetected" } - ListElement { interfaceName: "o2sensor"; stateName: "o2saturation" } - ListElement { interfaceName: "phsensor"; stateName: "ph" } - ListElement { interfaceName: "orpsensor"; stateName: "orp" } - ListElement { interfaceName: "vocsensor"; stateName: "voc" } - ListElement { interfaceName: "pm10sensor"; stateName: "pm10" } - ListElement { interfaceName: "pm25sensor"; stateName: "pm25" } - ListElement { interfaceName: "no2sensor"; stateName: "no2" } - ListElement { interfaceName: "o3sensor"; stateName: "o3" } - } - - delegate: RowLayout { - id: sensorValueDelegate - visible: itemDelegate.thing.thingClass.interfaces.indexOf(model.interfaceName) >= 0 - Layout.preferredWidth: contentItem.width / dataGrid.columns - - property StateType stateType: itemDelegate.thing.thingClass.stateTypes.findByName(model.stateName) - property State stateValue: stateType ? itemDelegate.thing.states.getState(stateType.id) : null - - property EventType eventType: itemDelegate.thing.thingClass.eventTypes.findByName(model.eventName) - LogsModel { - id: eventLogsModel - engine: sensorValueDelegate.eventType != null ? _engine : null - thingId: itemDelegate.thing.id - typeIds: sensorValueDelegate.eventType != null ? [sensorValueDelegate.eventType.id] : [] - live: true - fetchBlockSize: 1 - } - - ColorIcon { - Layout.preferredHeight: Style.iconSize - Layout.preferredWidth: height - Layout.alignment: Qt.AlignVCenter - color: { - switch (model.interfaceName) { - case "closablesensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Style.green : Style.red; - case "firesensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Style.red : Style.iconColor; - default: - return app.interfaceToColor(model.interfaceName) - } - } - name: { - switch (model.interfaceName) { - case "closablesensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? Qt.resolvedUrl("../images/lock-closed.svg") : Qt.resolvedUrl("../images/lock-open.svg"); - default: - return app.interfacesToIcon([model.interfaceName, "sensor"]) - } - } - } - - Label { - Layout.fillWidth: true - property var unit: sensorValueDelegate.stateType ? sensorValueDelegate.stateType.unit : Types.UnitNone - text: { - switch (model.interfaceName) { - case "closablesensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Closed") : qsTr("Open"); - case "presencesensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Presence") : qsTr("Vacant"); - case "daylightsensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Daytime") : qsTr("Nighttime"); - case "watersensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Wet") : qsTr("Dry"); - case "firesensor": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("Fire") : qsTr("No fire"); - case "heating": - return sensorValueDelegate.stateValue && sensorValueDelegate.stateValue.value === true ? qsTr("On") : qsTr("Off"); - case "vibrationsensor": { - if (eventLogsModel.count > 0) { - return qsTr("Last vibration: %1").arg(eventLogsModel.get(0).timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)) - } else { - return qsTr("Not moved yet") - } - } - default: - return sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" - ? sensorValueDelegate.stateType.displayName - : sensorValueDelegate.stateValue - ? "%1 %2".arg(Math.round(Types.toUiValue(sensorValueDelegate.stateValue.value, unit) * 100) / 100).arg(Types.toUiUnit(unit)) - : "" - } - } - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - font.pixelSize: app.smallFont - } - Led { - id: led - visible: sensorValueDelegate.stateType && sensorValueDelegate.stateType.type.toLowerCase() === "bool" && ["presencesensor", "daylightsensor", "heating", "closablesensor", "watersensor", "firesensor"].indexOf(model.interfaceName) < 0 - state: visible && sensorValueDelegate.stateValue.value === true ? "on" : "off" - } - Item { - Layout.preferredWidth: led.width - visible: led.visible - } - - } - } - } } } } diff --git a/nymea-app/ui/devicepages/EvChargerThingPage.qml b/nymea-app/ui/devicepages/EvChargerThingPage.qml index b111ec60..549eace8 100644 --- a/nymea-app/ui/devicepages/EvChargerThingPage.qml +++ b/nymea-app/ui/devicepages/EvChargerThingPage.qml @@ -90,7 +90,7 @@ ThingPageBase { } } - Dial { + StateDial { anchors.centerIn: parent height: background.contentItem.height width: background.contentItem.width diff --git a/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml b/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml index b7563a7f..7cdde196 100644 --- a/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml +++ b/nymea-app/ui/devicepages/FingerprintReaderDevicePage.qml @@ -317,7 +317,7 @@ ThingPageBase { Component { id: detailsPopup - MeaDialog { + NymeaDialog { id: detailsDialog property string timestamp property bool accessGranted diff --git a/nymea-app/ui/devicepages/NotificationsDevicePage.qml b/nymea-app/ui/devicepages/NotificationsDevicePage.qml index 7be0d1a3..24f71bdf 100644 --- a/nymea-app/ui/devicepages/NotificationsDevicePage.qml +++ b/nymea-app/ui/devicepages/NotificationsDevicePage.qml @@ -234,7 +234,7 @@ ThingPageBase { Component { id: detailsPopup - MeaDialog { + NymeaDialog { id: detailsDialog standardButtons: Dialog.NoButton property string timestamp diff --git a/nymea-app/ui/devicepages/SensorDevicePage.qml b/nymea-app/ui/devicepages/SensorDevicePage.qml index 2e0e8b46..11ad9d7a 100644 --- a/nymea-app/ui/devicepages/SensorDevicePage.qml +++ b/nymea-app/ui/devicepages/SensorDevicePage.qml @@ -111,92 +111,16 @@ ThingPageBase { id: flowRepeater model: sensorsModel - delegate: Item { + delegate: SensorView { width: Math.floor(flow.width / itemsInRow) height: Math.min(400, flow.cellWidth) property int row: Math.floor(index / flow.columns) property int itemsInRow: row < flow.totalRows ? flow.columns : (flowRepeater.count % flow.columns) - CircleBackground { - id: background - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - Style.margins - height: width - - readonly property StateType sensorStateType: root.thing.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) - readonly property State sensorState: root.thing.stateByName(interfaceStateMap[modelData]) - - onColor: app.interfaceToColor(modelData) - on: sensorStateType.type.toLowerCase() == "bool" && sensorState.value === true - iconSource: [ - "closablesensor", - "presencesensor", - "daylightsensor", - "firesensor", - "watersensor" - ].indexOf(modelData) >= 0 ? app.interfaceToIcon(modelData) : "" - - Loader { - anchors.centerIn: parent - width: background.contentItem.width - height: background.contentItem.height - property StateType stateType: root.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) - property State state: root.thing.stateByName(interfaceStateMap[modelData]) - property string interfaceName: modelData - property var minValue: { - if (["temperaturesensor"].indexOf(modelData) >= 0) { - return Types.toUiValue(-50, Types.UnitDegreeCelsius) - } - if (["pressuresensor"].indexOf(modelData) >= 0) { - return Types.toUiValue(900, Types.UnitMilliBar) - } - - return state.minValue - } - property var maxValue: { - if (["temperaturesensor"].indexOf(modelData) >= 0) { - return Types.toUiValue(50, Types.UnitDegreeCelsius) - } - if (["pressuresensor"].indexOf(modelData) >= 0) { - return Types.toUiValue(1100, Types.UnitMilliBar) - } - return state.maxValue - } - - sourceComponent: { - var progressInterfaces = [ - "humiditysensor", - "o2sensor", - "temperaturesensor", - "moisturesensor", - "conductivitysensor", - "gassensor", - "lightsensor", - "orpsensor", - "phsensor", - "pressuresensor", - "waterlevelsensor", - "windspeedsensor" - ] - if (progressInterfaces.indexOf(modelData) >= 0) { - return progressComponent - } - var scaleInterfaces = [ - "vocsensor", - "cosensor", - "co2sensor", - "o3sensor", - "pm10sensor", - "pm25sensor", - "no2sensor" - ] - if (scaleInterfaces.indexOf(modelData) >= 0) { - return scaleComponent - } - } - } - } + thing: root.thing + interfaceName: modelData } + } } @@ -218,7 +142,6 @@ ThingPageBase { property State state: root.thing.stateByName(interfaceStateMap[modelData]) property string interfaceName: modelData - // sourceComponent: stateType && stateType.type.toLowerCase() === "bool" ? boolComponent : graphComponent sourceComponent: graphComponent } @@ -233,7 +156,12 @@ ThingPageBase { id: graph thing: root.thing color: app.interfaceToColor(interfaceName) - iconSource: app.interfaceToIcon(interfaceName) + iconSource: { + if (graph.interfaceName == "closablesensor") { + return graph.state.value === true ? "sensors/window-closed" : "sensors/window-open" + } + return app.interfaceToIcon(interfaceName) + } implicitHeight: width * .6 property string interfaceName: parent.interfaceName stateType: parent.stateType @@ -260,365 +188,6 @@ ThingPageBase { } } } - - Component { - id: boolComponent - GridLayout { - id: boolView - property string interfaceName: parent.interfaceName - property StateType stateType: parent.stateType - height: listView.height - columns: app.landscape ? 2 : 1 - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumWidth: Style.iconSize * 5 - Layout.rowSpan: app.landscape ? 5 : 1 - ColorIcon { - anchors.centerIn: parent - height: Style.iconSize * 4 - width: height - name: { - switch (boolView.interfaceName) { - case "closablesensor": - return thing.states.getState(boolView.stateType.id).value === true ? Qt.resolvedUrl("../images/lock-closed.svg") : Qt.resolvedUrl("../images/lock-open.svg") - default: - return app.interfaceToIcon(boolView.interfaceName) - } - } - color: { - switch (boolView.interfaceName) { - case "closablesensor": - return thing.states.getState(boolView.stateType.id).value === true ? "green" : "red" - default: - thing.states.getState(boolView.stateType.id).value === true ? app.interfaceToColor(boolView.interfaceName) : Style.iconColor - } - } - } - } - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - RowLayout { - Layout.fillWidth: false - Layout.alignment: Qt.AlignHCenter - property StateType lastSeenStateType: root.thingClass.stateTypes.findByName("lastSeenTime") - property State lastSeenState: lastSeenStateType ? root.thing.states.getState(lastSeenStateType.id) : null - visible: lastSeenStateType !== null - Label { - text: qsTr("Last seen:") - font.bold: true - } - Label { - text: parent.lastSeenState ? Qt.formatDateTime(new Date(parent.lastSeenState.value * 1000)) : "" - } - } - RowLayout { - Layout.fillWidth: false - Layout.alignment: Qt.AlignHCenter - property StateType sunriseStateType: root.thingClass.stateTypes.findByName("sunriseTime") - property State sunriseState: sunriseStateType ? root.thing.states.getState(sunriseStateType.id) : null - visible: sunriseStateType !== null - Label { - text: qsTr("Sunrise:") - font.bold: true - } - Label { - text: parent.sunriseStateType ? Qt.formatDateTime(new Date(parent.sunriseState.value * 1000)) : "" - } - } - RowLayout { - Layout.fillWidth: false - Layout.alignment: Qt.AlignHCenter - property StateType sunsetStateType: root.thingClass.stateTypes.findByName("sunsetTime") - property State sunsetState: sunsetStateType ? root.thing.states.getState(sunsetStateType.id) : null - visible: sunsetStateType !== null - Label { - text: qsTr("Sunset:") - font.bold: true - } - Label { - text: parent.sunsetStateType ? Qt.formatDateTime(new Date(parent.sunsetState.value * 1000)) : "" - } - } - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - } - } - } - - Component { - id: progressComponent - Canvas { - id: progressCanvas - property string interfaceName: parent.interfaceName - property StateType stateType: parent.stateType - property State state: parent.state - property var minValue: parent.minValue - property var maxValue: parent.maxValue - - property double progress: (progressCanvas.state.value - progressCanvas.minValue) / (progressCanvas.maxValue - progressCanvas.minValue) - Behavior on progress { NumberAnimation { duration: Style.slowAnimationDuration; easing.type: Easing.InOutQuad } } - onProgressChanged: requestPaint(); - - ColumnLayout { - anchors.centerIn: parent - anchors.verticalCenterOffset: -Style.smallMargins - width: parent.width * 0.6 - - Label { - Layout.fillWidth: true - text: Types.toUiValue(progressCanvas.state.value, progressCanvas.stateType.unit).toFixed(1) - wrapMode: Text.WordWrap - font.pixelSize: Math.min(Style.hugeFont.pixelSize, progressCanvas.height / 8) - maximumLineCount: 2 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - Label { - Layout.fillWidth: true - text: Types.toUiUnit(progressCanvas.stateType.unit) - font.pixelSize: Math.min(Style.largeFont.pixelSize, progressCanvas.height / 12) - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - } - - - onPaint: { - var ctx = getContext("2d"); - ctx.reset(); - - ctx.beginPath() - ctx.fillStyle = Style.foregroundColor - - ctx.translate(width / 2, height / 2) - ctx.rotate(135 * Math.PI / 180) - - ctx.lineCap = "round" - ctx.lineWidth = width * .1 - - ctx.beginPath() - ctx.strokeStyle = Style.tileOverlayColor - var startAngle = 0 - var endAngle = 270 - var radStart = startAngle * Math.PI/180; - var radEnd = endAngle * Math.PI/180; - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) - ctx.stroke() - ctx.closePath() - - ctx.beginPath() - ctx.strokeStyle = app.interfaceToColor(progressCanvas.interfaceName) - radEnd *= progressCanvas.progress - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) - ctx.stroke() - ctx.closePath() - } - - ColorIcon { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Style.smallMargins - name: app.interfaceToIcon(progressCanvas.interfaceName) - size: Math.min(Style.bigIconSize, parent.height / 5) - color: app.interfaceToColor(progressCanvas.interfaceName) - } - } - } - - Component { - id: scaleComponent - Item { - id: scaleCanvas - property string interfaceName: parent.interfaceName - property StateType stateType: parent.stateType - property State state: parent.state - property var minValue: parent.minValue - property var maxValue: parent.maxValue - - property int scaleWidth: width * .1 - - property var scale: { - switch (interfaceName) { - case "vocsensor": - return AirQualityIndex.iaqVoc - case "cosensor": - return AirQualityIndex.caqiCo - case "o3sensor": - return AirQualityIndex.caqiO3 - case "pm10sensor": - return AirQualityIndex.caqiPm10 - case "pm25sensor": - return AirQualityIndex.caqiPm25 - case "no2sensor": - return AirQualityIndex.caqiNo2 - } - return baseScale - } - property var baseScale: [ - { - "value": maxValue, - "angle": 270, - "color": Style.tileOverlayColor - } - ] - - property var currentIndex: { - for (var i = 0; i < scale.length; i++) { - if (state.value <= scale[i].value) { - return i; - } - } - log.warn("Value out of scale!") - return -1 - } - - property double angle: { - var baseAngle = 0 - var baseValue = 0; - if (currentIndex > 0) { - baseAngle = scale[currentIndex-1].angle - baseValue = scale[currentIndex-1].value - } - var valueRange = scale[currentIndex].value - baseValue - var angleRange = scale[currentIndex].angle - baseAngle - var progress = (state.value - baseValue) / (scale[currentIndex].value - baseValue) - return baseAngle + angleRange * progress - } - Behavior on angle { NumberAnimation { duration: Style.slowAnimationDuration; easing.type: Easing.InOutQuad } } - onAngleChanged: maskCanvas.requestPaint(); - - Canvas { - id: baseCanvas - anchors.fill: parent - visible: false - onPaint: { - var ctx = getContext("2d"); - ctx.reset(); - - ctx.beginPath() - ctx.fillStyle = Style.foregroundColor - - ctx.translate(width / 2, height / 2) - ctx.rotate(135 * Math.PI / 180) - - ctx.lineCap = "round" - ctx.lineWidth = scaleCanvas.scaleWidth - - // paint first rounded - ctx.beginPath() - ctx.strokeStyle = scaleCanvas.scale[0].color - var startAngle = 0 - var endAngle = scaleCanvas.scale[0].angle * Math.PI/180 - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) - ctx.stroke() - ctx.closePath() - - // paint last rounded - ctx.beginPath() - ctx.strokeStyle = scaleCanvas.scale[scaleCanvas.scale.length - 1].color - startAngle = scaleCanvas.scale[scaleCanvas.scale.length - 2].angle * Math.PI/180 - endAngle = scaleCanvas.scale[scaleCanvas.scale.length - 1].angle * Math.PI/180 - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) - ctx.stroke() - ctx.closePath() - - // paint inner parts - ctx.lineCap = "butt" - for (var i = 1; i < scaleCanvas.scale.length - 1; i++) { - ctx.beginPath() - ctx.strokeStyle = scaleCanvas.scale[i].color - startAngle = scaleCanvas.scale[i - 1].angle * Math.PI/180 - endAngle = scaleCanvas.scale[i].angle * Math.PI/180 - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, startAngle, endAngle) - ctx.stroke() - ctx.closePath() - } - } - } - Canvas { - id: maskCanvas - anchors.fill: parent - visible: false - onPaint: { - var ctx = getContext("2d"); - ctx.reset(); - - ctx.beginPath() - ctx.fillStyle = Style.foregroundColor - - ctx.translate(width / 2, height / 2) - ctx.rotate(135 * Math.PI / 180) - - ctx.lineCap = "round" - ctx.lineWidth = width * .1 - - ctx.beginPath() - ctx.strokeStyle = "#55ffffff" - var startAngle = 0 - var endAngle = 270 - var radStart = startAngle * Math.PI/180; - var radEnd = endAngle * Math.PI/180; - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) - ctx.stroke() - ctx.closePath() - - ctx.beginPath() - ctx.strokeStyle = "#000000" - radEnd = scaleCanvas.angle * Math.PI/180 - ctx.arc(0, 0, width / 2 - ctx.lineWidth / 2, radStart, radEnd) - ctx.stroke() - ctx.closePath() - } - } - - OpacityMask { - anchors.fill: parent - source: baseCanvas - maskSource: maskCanvas - } - - - ColumnLayout { - anchors.centerIn: parent - anchors.verticalCenterOffset: -Style.smallMargins - width: parent.width * 0.6 - - Label { - Layout.fillWidth: true - text: scaleCanvas.scale[scaleCanvas.currentIndex].text - font.pixelSize: Math.min(Style.hugeFont.pixelSize, scaleCanvas.height / 8) - wrapMode: Text.WordWrap - // color: scaleCanvas.scale[scaleCanvas.currentIndex].color - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - maximumLineCount: 2 - } - Label { - Layout.fillWidth: true - text: Types.toUiValue(scaleCanvas.state.value, scaleCanvas.stateType.unit).toFixed(1) + " " + Types.toUiUnit(scaleCanvas.stateType.unit) - font.pixelSize: Math.min(Style.largeFont.pixelSize, scaleCanvas.height / 12) - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - } - - - ColorIcon { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Style.smallMargins - // anchors.verticalCenterOffset: scaleCanvas.height / 2 - height - name: app.interfaceToIcon(scaleCanvas.interfaceName) - size: Math.min(Style.bigIconSize, parent.height / 5) - color: app.interfaceToColor(scaleCanvas.interfaceName) - } - } } } diff --git a/nymea-app/ui/devicepages/ThermostatDevicePage.qml b/nymea-app/ui/devicepages/ThermostatDevicePage.qml index ddfc4c38..e39e2a1d 100644 --- a/nymea-app/ui/devicepages/ThermostatDevicePage.qml +++ b/nymea-app/ui/devicepages/ThermostatDevicePage.qml @@ -56,13 +56,14 @@ ThingPageBase { GridLayout { anchors.fill: parent - anchors.margins: app.margins + anchors.margins: Style.margins columns: app.landscape ? 2 : 1 CircleBackground { Layout.fillWidth: true Layout.fillHeight: true Layout.margins: Style.bigMargins + ThermostatController { anchors.centerIn: parent height: Math.min(400, Math.min(parent.height, parent.width)) diff --git a/nymea-app/ui/devicepages/VentilationThingPage.qml b/nymea-app/ui/devicepages/VentilationThingPage.qml index e4d71351..5766b266 100644 --- a/nymea-app/ui/devicepages/VentilationThingPage.qml +++ b/nymea-app/ui/devicepages/VentilationThingPage.qml @@ -39,6 +39,7 @@ ThingPageBase { id: root readonly property State powerState: thing.stateByName("power") + readonly property State autoState: thing.stateByName("auto") readonly property State flowRateState: thing.stateByName("flowRate") readonly property StateType flowRateStateType: thing.thingClass.stateTypes.findByName("flowRate") @@ -48,40 +49,103 @@ ThingPageBase { stateName: "power" } - CircleBackground { - id: background + GridLayout { anchors.fill: parent - anchors.margins: Style.hugeMargins - iconSource: "ventilation" - onColor: app.interfaceToColor("ventilation") - showOnGradient: root.flowRateState == null - on: (actionQueue.pendingValue || powerState.value) === true - onClicked: { - PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) - actionQueue.sendValue(!root.powerState.value) + anchors.margins: Style.margins + columns: app.landscape ? 2 : 1 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Style.bigMargins + implicitWidth: 400 + implicitHeight: 400 + + CircleBackground { + id: background + anchors.fill: parent + iconSource: "ventilation" + onColor: app.interfaceToColor("ventilation") + showOnGradient: root.flowRateState == null + on: (actionQueue.pendingValue || powerState.value) === true + PropertyAnimation on rotation { + running: root.powerState.value === true + duration: 2000 + from: 0 + to: 360 + loops: Animation.Infinite + onDurationChanged: { + running = false; + running = true; + } + } + } + + Dial { + anchors.centerIn: background + height: background.contentItem.height + width: background.contentItem.width + visible: root.flowRateState + on: (actionQueue.pendingValue || powerState.value) === true + value: valueActionQueue.pendingValue || flowRateState.value + onMoved: valueActionQueue.sendValue(value) + color: app.interfaceToColor("ventilation") + minValue: root.flowRateState.minValue + maxValue: root.flowRateState.maxValue + + onClicked: { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + actionQueue.sendValue(!root.powerState.value) + } + + ActionQueue { + id: valueActionQueue + thing: root.thing + stateName: "flowRate" + + } + } + + +// StateDial { +// anchors.centerIn: background +// height: background.contentItem.height +// width: background.contentItem.width +// visible: root.flowRateState +// on: (actionQueue.pendingValue || powerState.value) === true + +// thing: root.thing +// stateName: "flowRate" +// color: app.interfaceToColor("ventilation") +// } } - PropertyAnimation on rotation { - running: root.powerState.value === true - duration: 2000 - from: 0 - to: 360 - loops: Animation.Infinite - onDurationChanged: { - running = false; - running = true; + + ProgressButton { + Layout.alignment: Qt.AlignHCenter + Layout.margins: Style.bigMargins + size: Style.largeIconSize + imageSource: "" + color: Style.white + backgroundColor: Style.accentColor + visible: root.autoState + busy: autoActionQueue.pendingValue ? autoActionQueue.pendingValue : (root.autoState && root.autoState.value === true) + onClicked: autoActionQueue.sendValue(!root.autoState.value) + + Label { + anchors.centerIn: parent + text: "A" + font.pixelSize: parent.height / 2 + } + + ActionQueue { + id: autoActionQueue + thing: root.thing + stateName: "auto" } } } - Dial { - anchors.centerIn: parent - height: background.contentItem.height - width: background.contentItem.width - visible: root.flowRateState - on: (actionQueue.pendingValue || powerState.value) === true - thing: root.thing - stateName: "flowRate" - color: app.interfaceToColor("ventilation") - } + + } diff --git a/nymea-app/ui/images/calendar.svg b/nymea-app/ui/images/calendar.svg new file mode 100644 index 00000000..30a2b3ac --- /dev/null +++ b/nymea-app/ui/images/calendar.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/edit-paste.svg b/nymea-app/ui/images/edit-paste.svg new file mode 100644 index 00000000..a12b24fa --- /dev/null +++ b/nymea-app/ui/images/edit-paste.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/infinity.svg b/nymea-app/ui/images/infinity.svg new file mode 100644 index 00000000..bf93d8e0 --- /dev/null +++ b/nymea-app/ui/images/infinity.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/sensors/window-closed.svg b/nymea-app/ui/images/sensors/window-closed.svg new file mode 100644 index 00000000..86fb748f --- /dev/null +++ b/nymea-app/ui/images/sensors/window-closed.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/nymea-app/ui/images/sensors/window-open.svg b/nymea-app/ui/images/sensors/window-open.svg new file mode 100644 index 00000000..a1f77d5f --- /dev/null +++ b/nymea-app/ui/images/sensors/window-open.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/magic/EditRulePage.qml b/nymea-app/ui/magic/EditRulePage.qml index 929e6df0..9bd0da3e 100644 --- a/nymea-app/ui/magic/EditRulePage.qml +++ b/nymea-app/ui/magic/EditRulePage.qml @@ -275,7 +275,7 @@ Page { return; } print("Rule has changed. Asking for cancellation dialog") - var component = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var component = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var popup = component.createObject(root, {headerIcon: "../images/question.svg", title: qsTr("Cancel?"), text: qsTr("Any changes to the rule will be lost."), diff --git a/nymea-app/ui/magic/ScriptEditor.qml b/nymea-app/ui/magic/ScriptEditor.qml index 4a88d8db..8d15e691 100644 --- a/nymea-app/ui/magic/ScriptEditor.qml +++ b/nymea-app/ui/magic/ScriptEditor.qml @@ -50,7 +50,7 @@ Page { } if ((Qt.platform.os == "android" || Qt.platform.os == "ios") && !editorSettings.popupWasShown) { - var component = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var component = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var infoPopup = component.createObject(root, { title: qsTr("Did you know..."), @@ -75,7 +75,7 @@ Page { pageStack.pop() return; } - var comp = Qt.createComponent("../components/MeaDialog.qml"); + var comp = Qt.createComponent("../components/NymeaDialog.qml"); var popup = comp.createObject(root, { title: qsTr("Unsaved changes"), text: qsTr("There are unsaved changes in the script. Do you want to discard the changes?"), diff --git a/nymea-app/ui/mainviews/AirConditioningView.qml b/nymea-app/ui/mainviews/AirConditioningView.qml new file mode 100644 index 00000000..1f01d792 --- /dev/null +++ b/nymea-app/ui/mainviews/AirConditioningView.qml @@ -0,0 +1,128 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU 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 General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtCharts 2.2 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" +import "qrc:/ui/delegates" +import "airconditioning" + +MainViewBase { + id: root + + contentY: flickable.contentY + topMargin + + headerButtons: [ + { + iconSource: "/ui/images/configure.svg", + color: Style.iconColor, + visible: acManager.zoneInfos.count > 0, + trigger: function() { + pageStack.push("airconditioning/ACSettingsPage.qml", {acManager: acManager}); + } + } + ] + + LoggingCategory { + id: category + name: "AirConditioning" + } + + + ThingsProxy { + id: thermostats + engine: _engine + shownInterfaces: ["thermostat"] + } + + + AirConditioningManager { + id: acManager + engine: _engine + } + + ZonesView { + id: flickable + anchors.fill: parent + topMargin: root.topMargin + bottomMargin: root.bottomMargin + clip: true + acManager: acManager + } + + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + visible: !engine.thingManager.fetchingData && (!engine.jsonRpcClient.experiences.hasOwnProperty("AirConditioning") || engine.jsonRpcClient.experiences["AirConditioning"] < "0.1") + title: qsTr("Air conditioning plugin not installed.") + text: qsTr("To set up air conditioning, install the air conditioning plugin.") + imageSource: "../images/smartmeter.svg" + buttonText: qsTr("Install A/C plugin") + buttonVisible: packagesFilterModel.count > 0 + onButtonClicked: pageStack.push(Qt.resolvedUrl("../system/PackageListPage.qml"), {filter: "nymea-experience-plugin-airconditioning"}) + PackagesFilterModel { + id: packagesFilterModel + packages: engine.systemController.packages + nameFilter: "nymea-experience-plugin-airconditioning" + } + } + EmptyViewPlaceholder { + id: noZonePlaceHolder + anchors.centerIn: parent + width: parent.width - app.margins * 2 + visible: engine.jsonRpcClient.experiences["AirConditioning"] >= "0.1" && acManager.zoneInfos.count == 0 + title: qsTr("No zones configured.") + text: qsTr("Start with configuring your zones.") + imageSource: "../images/sensors.svg" + buttonText: qsTr("Add zone") + onButtonClicked: { + pendingAddCall = acManager.addZone(qsTr("Zone %1").arg(acManager.zoneInfos.count + 1), [], [], [], []) + } + property int pendingAddCall: -1 + + Connections { + target: acManager + onAddZoneReply: { + if (commandId == noZonePlaceHolder.pendingAddCall) { + print("zone added", zoneId) + var zone = acManager.zoneInfos.getZoneInfo(zoneId) + pageStack.push(Qt.resolvedUrl("airconditioning/EditZonePage.qml"), {acManager: acManager, zone: zone, createNew: true}) + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml b/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml new file mode 100644 index 00000000..37e5e744 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml @@ -0,0 +1,777 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import QtCharts 2.3 + +Page { + id: root + property AirConditioningManager acManager: null + property ZoneInfoWrapper zoneWrapper: null + readonly property ZoneInfo zone: zoneWrapper.zone + + + header: NymeaHeader { + text: root.zone.name + + onBackPressed: { + pageStack.pop() + } + } + + Component { + id: lineSeriesComponent + LineSeries { } + } + + QtObject { + id: d + + property date now: new Date() + + property int range: 60 * 24 + + readonly property var startTime: { + var date = new Date(now); + date.setTime(date.getTime() - range * 60000 + 2000); + return date; + } + + readonly property var endTime: { + var date = new Date(now); + date.setTime(date.getTime() + 2000) + return date; + } + } + + + ChartView { + id: chartView + anchors.fill: parent + + backgroundColor: "transparent" + margins.left: 0 + margins.right: 0 + margins.top: 0 + margins.bottom: Style.smallIconSize + Style.margins + + legend.visible: false + legend.alignment: Qt.AlignBottom + legend.font: Style.extraSmallFont + legend.labelColor: Style.foregroundColor + + ValueAxis { + id: temperatureAxis + min: 0 + max: 50 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + } + + ValueAxis { + id: humidityAxis + min: 0 + max: 100 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + visible: false + } + + ValueAxis { + id: vocAxis + min: 0 + max: 1000 + // max: vocRepeater.count > 0 ? vocRepeater.itemAt(0).logsModel.maxValue : 0 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + visible: false + } + + ValueAxis { + id: boolAxis + min: 0 + max: 1 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + visible: false + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: chartView.plotArea.y + height: chartView.plotArea.height + width: chartView.plotArea.x - x + Repeater { + model: temperatureAxis.tickCount + delegate: ColumnLayout { + y: index == temperatureAxis.tickCount - 1 + ? parent.height - height + : index == 0 + ? 0 + : parent.height / (temperatureAxis.tickCount - 1) * index - height / 2 + Label { + width: parent.width - Style.smallMargins + horizontalAlignment: Text.AlignRight + text: (temperatureAxis.max - (index * temperatureAxis.max / (temperatureAxis.tickCount - 1))) + "°C" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + visible: tempRepeater.count > 0 || thermostatsRepeater.count > 0 + color: app.interfaceToColor("temperaturesensor") + } + Label { + width: parent.width - Style.smallMargins + horizontalAlignment: Text.AlignRight + text: (humidityAxis.max - (index * humidityAxis.max / (humidityAxis.tickCount - 1))).toFixed(0) + "%" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + visible: humidityRepeater.count > 0 + color: app.interfaceToColor("humiditysensor") + } + Label { + width: parent.width - Style.smallMargins + horizontalAlignment: Text.AlignRight + text: (vocAxis.max - (index * vocAxis.max / (vocAxis.tickCount - 1))).toFixed(0) + "ppm" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + visible: vocRepeater.count > 0 + color: app.interfaceToColor("vocsensor") + } + } + } + } + + DateTimeAxis { + id: dateTimeAxis + min: d.startTime + max: d.endTime + format: { + // switch (selectionTabs.currentValue.sampleRate) { + // case EnergyLogs.SampleRate1Min: + // case EnergyLogs.SampleRate15Mins: + return "hh:mm" + // case EnergyLogs.SampleRate1Hour: + // case EnergyLogs.SampleRate3Hours: + // case EnergyLogs.SampleRate1Day: + // return "dd.MM." + // } + } + tickCount: { + // switch (selectionTabs.currentValue.sampleRate) { + // case EnergyLogs.SampleRate1Min: + // case EnergyLogs.SampleRate15Mins: + // return root.width > 500 ? 13 : 7 + // case EnergyLogs.SampleRate1Hour: + // return 7 + // case EnergyLogs.SampleRate3Hours: + // case EnergyLogs.SampleRate1Day: + return root.width > 500 ? 12 : 6 + // } + } + labelsFont: Style.extraSmallFont + gridVisible: false + minorGridVisible: false + lineVisible: false + shadesVisible: false + labelsColor: Style.foregroundColor + } + + Repeater { + id: thermostatsRepeater + model: zoneWrapper.thermostats + delegate: Item { + id: thermostatDelegate + readonly property Thing thing: zoneWrapper.thermostats.get(index) + property XYSeries series: null + + readonly property LogsModel logsModel: LogsModel { + objectName: "temp: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("temperature").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: thermostatDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, temperatureAxis) + series.color = app.interfaceToColor("temperaturesensor") + series.width = 2 + + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + id: tempRepeater + model: zoneWrapper.indoorTempSensors + delegate: Item { + id: tempDelegate + readonly property Thing thing: zoneWrapper.indoorTempSensors.get(index) + property XYSeries series: null + + readonly property LogsModel logsModel: LogsModel { + objectName: "temp: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("temperature").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: tempDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, temperatureAxis) + series.color = app.interfaceToColor("temperaturesensor") + series.width = 1 + + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + id: humidityRepeater + model: zoneWrapper.indoorHumiditySensors + delegate: Item { + id: humidityDelegate + readonly property Thing thing: zoneWrapper.indoorHumiditySensors.get(index) + property XYSeries series: null + + readonly property LogsModel logsModel: LogsModel { + objectName: "hum: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("humidity").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: humidityDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, humidityAxis) + series.color = app.interfaceToColor("humiditysensor") + series.width = 1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + id: vocRepeater + model: zoneWrapper.indoorVocSensors + delegate: Item { + id: vocDelegate + readonly property Thing thing: zoneWrapper.indoorVocSensors.get(index) + property XYSeries series: null + readonly property LogsModel logsModel: LogsModel { + objectName: "voc: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("voc").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: vocDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, vocAxis) + series.color = app.interfaceToColor("vocsensor") + series.width = 1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + model: zoneWrapper.windowSensors + delegate: Item { + id: closableDelegate + readonly property Thing thing: zoneWrapper.windowSensors.get(index) + property AreaSeries series: null + + LineSeries { + id: closableUpperSeries + } + LineSeries { + id: closableLowerSeries + XYPoint {x: dateTimeAxis.min.getTime(); y: 0} + XYPoint {x: dateTimeAxis.max.getTime(); y: 0} + } + + readonly property LogsModel logsModel: LogsModel { + id: logsModelNg + engine: typeIds.length ? _engine : null + thingId: thing ? thing.id : "" + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("closed").id) + return ret; + } + live: true + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + } + + BoolSeriesAdapter { + logsModel: closableDelegate.logsModel + xySeries: closableUpperSeries + inverted: true + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, boolAxis) + series.lowerSeries = closableLowerSeries + series.upperSeries = closableUpperSeries + series.color = Style.green + series.opacity = 0.1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + } + } + + Repeater { + model: zoneWrapper.thermostats.count + delegate: Item { + id: heatingDelegate + readonly property Thing thing: zoneWrapper.thermostats.get(index) + property AreaSeries series: null + + + LineSeries { + id: heatingUpperSeries + } + LineSeries { + id: heatingLowerSeries + XYPoint {x: dateTimeAxis.max.getTime(); y: 0} + XYPoint {x: dateTimeAxis.min.getTime(); y: 0} + + } + + readonly property LogsModel logsModel: LogsModel { + objectName: "heat: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing ? thing.id : "" + typeIds: { + var ret = []; + var heatingOnStateType = thing.thingClass.stateTypes.findByName("heatingOn") + print("**** has heatingOn") + if (heatingOnStateType) { + print("**** true") + ret.push(heatingOnStateType.id) + } + return ret; + } + live: true + // graphSeries: heatingUpperSeries + viewStartTime: dateTimeAxis.min + } + + BoolSeriesAdapter { + logsModel: heatingDelegate.logsModel + xySeries: heatingUpperSeries + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, boolAxis) + series.lowerSeries = heatingLowerSeries + series.upperSeries = heatingUpperSeries + series.color = Style.red + series.opacity = 0.1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + } + } + } + + + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.leftMargin: chartView.plotArea.x + anchors.topMargin: chartView.plotArea.y + anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x + anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y + + hoverEnabled: true + preventStealing: tooltipping || dragging + + property int startMouseX: 0 + property bool dragging: false + property bool tooltipping: false + + property var startDatetime: null + + Timer { + interval: 300 + running: mouseArea.pressed + onTriggered: { + if (!mouseArea.dragging) { + mouseArea.tooltipping = true + } + } + } + onReleased: { + mouseArea.tooltipping = false; + + if (mouseArea.dragging) { + mouseArea.dragging = false; + } + } + + onPressed: { + startMouseX = mouseX + startDatetime = d.now + } + + + onDoubleClicked: { + if (selectionTabs.currentIndex == 0) { + return; + } + + var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width) + var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000)) + selectionTabs.currentIndex-- + d.now = new Date(Math.min(new Date().getTime(), timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)) + powerBalanceLogs.fetchLogs() + logsLoader.fetchLogs() + } + + onMouseXChanged: { + if (!pressed || mouseArea.tooltipping) { + return; + } + if (Math.abs(startMouseX - mouseX) < 10) { + return; + } + dragging = true + + var dragDelta = startMouseX - mouseX + var totalTime = d.endTime.getTime() - d.startTime.getTime() + // dragDelta : timeDelta = width : totalTime + var timeDelta = dragDelta * totalTime / mouseArea.width + print("dragging", dragDelta, totalTime, mouseArea.width) + d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta))) + } + + onWheel: { + startDatetime = d.now + var totalTime = d.endTime.getTime() - d.startTime.getTime() + // pixelDelta : timeDelta = width : totalTime + var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width + print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width) + d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta))) + wheelStopTimer.restart() + } + Timer { + id: wheelStopTimer + interval: 300 + repeat: false + onTriggered: { + // for (var i = 0; i < consumersRepeater.count; i++) { + // if (consumersRepeater.itemAt(i).logs.fetchingData) { + // wheelStopTimer.start() + // return; + // } + // } + + // powerBalanceLogs.fetchLogs() + // logsLoader.fetchLogs() + } + } + + Rectangle { + height: parent.height + width: 1 + color: Style.foregroundColor + x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX)) + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + } + + Item { + id: tooltips + anchors.fill: parent + property var timestamp: new Date(((d.endTime.getTime() - d.startTime.getTime()) * mouseArea.mouseX / mouseArea.width) + d.startTime.getTime()) + property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins + property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - tooltipWidth + + property int tooltipWidth: 130 + property int tooltipX: xOnLeft < 0 ? xOnRight : xOnLeft + + onTimestampChanged: { + updateTimer.start(); + } + Timer { + id: updateTimer + interval: 0 + onTriggered: tooltips.update() + } + + function update() { + var ordered = [] + insert(thermostatTooltipRepeater, ordered); + insert(tempTooltipRepeater, ordered); + insert(humidityTooltipRepeater, ordered); + insert(vocTooltipRepeater, ordered); + + for (var i = ordered.length - 1; i >= 0; i--) { + var item = ordered[i] + var newY = item.realY + + if (i < ordered.length-1) { + var previous = ordered[i+1] + newY = Math.min(newY, previous.fixedY - item.height/* - Style.extraSmallMargins*/) + } + + ordered[i].fixedY = newY + } + } + + function insert(repeater, array) { + for (var i = 0; i < repeater.count; i++) { + var item = repeater.itemAt(i); + var insertIdx = 0; + while (array.length > insertIdx && item.realY > array[insertIdx].realY) { + insertIdx++ + } + array.splice(insertIdx, 0, item) + } + } + } + + + Repeater { + id: thermostatTooltipRepeater + model: thermostatsRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: thermostatsRepeater.itemAt(index).thing + entry: thermostatsRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("temperaturesensor") + axis: temperatureAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitDegreeCelsius + } + } + + Repeater { + id: tempTooltipRepeater + model: tempRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: tempRepeater.itemAt(index).thing + entry: tempRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("temperaturesensor") + axis: temperatureAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitDegreeCelsius + } + } + + Repeater { + id: humidityTooltipRepeater + model: humidityRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: humidityRepeater.itemAt(index).thing + entry: humidityRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("humiditysensor") + axis: humidityAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitPercentage + } + } + + Repeater { + id: vocTooltipRepeater + model: vocRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: vocRepeater.itemAt(index).thing + entry: vocRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("vocsensor") + axis: vocAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitPartsPerMillion + } + } + } + + + RowLayout { + id: legend + anchors { left: parent.left; bottom: parent.bottom; right: parent.right } + anchors.leftMargin: chartView.plotArea.x + height: Style.smallIconSize + anchors.margins: Style.margins + + Repeater { + model: thermostatsRepeater.count + delegate: LegendDelegate { + thing: thermostatsRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("thermostat") + color: app.interfaceToColor("temperaturesensor") + } + } + Repeater { + model: tempRepeater.count + delegate: LegendDelegate { + thing: tempRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("temperaturesensor") + color: app.interfaceToColor("temperaturesensor") + } + } + Repeater { + model: humidityRepeater.count + delegate: LegendDelegate { + thing: humidityRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("humiditysensor") + color: app.interfaceToColor("humiditysensor") + } + } + Repeater { + model: vocRepeater.count + delegate: LegendDelegate { + thing: vocRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("vocsensor") + color: app.interfaceToColor("vocsensor") + } + } + } +} + diff --git a/nymea-app/ui/mainviews/airconditioning/ACChartsPage2.qml b/nymea-app/ui/mainviews/airconditioning/ACChartsPage2.qml new file mode 100644 index 00000000..37e5e744 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ACChartsPage2.qml @@ -0,0 +1,777 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import QtCharts 2.3 + +Page { + id: root + property AirConditioningManager acManager: null + property ZoneInfoWrapper zoneWrapper: null + readonly property ZoneInfo zone: zoneWrapper.zone + + + header: NymeaHeader { + text: root.zone.name + + onBackPressed: { + pageStack.pop() + } + } + + Component { + id: lineSeriesComponent + LineSeries { } + } + + QtObject { + id: d + + property date now: new Date() + + property int range: 60 * 24 + + readonly property var startTime: { + var date = new Date(now); + date.setTime(date.getTime() - range * 60000 + 2000); + return date; + } + + readonly property var endTime: { + var date = new Date(now); + date.setTime(date.getTime() + 2000) + return date; + } + } + + + ChartView { + id: chartView + anchors.fill: parent + + backgroundColor: "transparent" + margins.left: 0 + margins.right: 0 + margins.top: 0 + margins.bottom: Style.smallIconSize + Style.margins + + legend.visible: false + legend.alignment: Qt.AlignBottom + legend.font: Style.extraSmallFont + legend.labelColor: Style.foregroundColor + + ValueAxis { + id: temperatureAxis + min: 0 + max: 50 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + } + + ValueAxis { + id: humidityAxis + min: 0 + max: 100 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + visible: false + } + + ValueAxis { + id: vocAxis + min: 0 + max: 1000 + // max: vocRepeater.count > 0 ? vocRepeater.itemAt(0).logsModel.maxValue : 0 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + visible: false + } + + ValueAxis { + id: boolAxis + min: 0 + max: 1 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + visible: false + } + + Item { + id: labelsLayout + x: Style.smallMargins + y: chartView.plotArea.y + height: chartView.plotArea.height + width: chartView.plotArea.x - x + Repeater { + model: temperatureAxis.tickCount + delegate: ColumnLayout { + y: index == temperatureAxis.tickCount - 1 + ? parent.height - height + : index == 0 + ? 0 + : parent.height / (temperatureAxis.tickCount - 1) * index - height / 2 + Label { + width: parent.width - Style.smallMargins + horizontalAlignment: Text.AlignRight + text: (temperatureAxis.max - (index * temperatureAxis.max / (temperatureAxis.tickCount - 1))) + "°C" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + visible: tempRepeater.count > 0 || thermostatsRepeater.count > 0 + color: app.interfaceToColor("temperaturesensor") + } + Label { + width: parent.width - Style.smallMargins + horizontalAlignment: Text.AlignRight + text: (humidityAxis.max - (index * humidityAxis.max / (humidityAxis.tickCount - 1))).toFixed(0) + "%" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + visible: humidityRepeater.count > 0 + color: app.interfaceToColor("humiditysensor") + } + Label { + width: parent.width - Style.smallMargins + horizontalAlignment: Text.AlignRight + text: (vocAxis.max - (index * vocAxis.max / (vocAxis.tickCount - 1))).toFixed(0) + "ppm" + verticalAlignment: Text.AlignTop + font: Style.extraSmallFont + visible: vocRepeater.count > 0 + color: app.interfaceToColor("vocsensor") + } + } + } + } + + DateTimeAxis { + id: dateTimeAxis + min: d.startTime + max: d.endTime + format: { + // switch (selectionTabs.currentValue.sampleRate) { + // case EnergyLogs.SampleRate1Min: + // case EnergyLogs.SampleRate15Mins: + return "hh:mm" + // case EnergyLogs.SampleRate1Hour: + // case EnergyLogs.SampleRate3Hours: + // case EnergyLogs.SampleRate1Day: + // return "dd.MM." + // } + } + tickCount: { + // switch (selectionTabs.currentValue.sampleRate) { + // case EnergyLogs.SampleRate1Min: + // case EnergyLogs.SampleRate15Mins: + // return root.width > 500 ? 13 : 7 + // case EnergyLogs.SampleRate1Hour: + // return 7 + // case EnergyLogs.SampleRate3Hours: + // case EnergyLogs.SampleRate1Day: + return root.width > 500 ? 12 : 6 + // } + } + labelsFont: Style.extraSmallFont + gridVisible: false + minorGridVisible: false + lineVisible: false + shadesVisible: false + labelsColor: Style.foregroundColor + } + + Repeater { + id: thermostatsRepeater + model: zoneWrapper.thermostats + delegate: Item { + id: thermostatDelegate + readonly property Thing thing: zoneWrapper.thermostats.get(index) + property XYSeries series: null + + readonly property LogsModel logsModel: LogsModel { + objectName: "temp: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("temperature").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: thermostatDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, temperatureAxis) + series.color = app.interfaceToColor("temperaturesensor") + series.width = 2 + + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + id: tempRepeater + model: zoneWrapper.indoorTempSensors + delegate: Item { + id: tempDelegate + readonly property Thing thing: zoneWrapper.indoorTempSensors.get(index) + property XYSeries series: null + + readonly property LogsModel logsModel: LogsModel { + objectName: "temp: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("temperature").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: tempDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, temperatureAxis) + series.color = app.interfaceToColor("temperaturesensor") + series.width = 1 + + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + id: humidityRepeater + model: zoneWrapper.indoorHumiditySensors + delegate: Item { + id: humidityDelegate + readonly property Thing thing: zoneWrapper.indoorHumiditySensors.get(index) + property XYSeries series: null + + readonly property LogsModel logsModel: LogsModel { + objectName: "hum: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("humidity").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: humidityDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, humidityAxis) + series.color = app.interfaceToColor("humiditysensor") + series.width = 1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + id: vocRepeater + model: zoneWrapper.indoorVocSensors + delegate: Item { + id: vocDelegate + readonly property Thing thing: zoneWrapper.indoorVocSensors.get(index) + property XYSeries series: null + readonly property LogsModel logsModel: LogsModel { + objectName: "voc: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing.id + live: true + // graphSeries: series + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + fetchBlockSize: 500 + + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("voc").id) + return ret; + } + } + + XYSeriesAdapter { + logsModel: vocDelegate.logsModel + xySeries: series + sampleRate: XYSeriesAdapter.SampleRate10Minutes + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeLine, thing.name, dateTimeAxis, vocAxis) + series.color = app.interfaceToColor("vocsensor") + series.width = 1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + + } + } + + Repeater { + model: zoneWrapper.windowSensors + delegate: Item { + id: closableDelegate + readonly property Thing thing: zoneWrapper.windowSensors.get(index) + property AreaSeries series: null + + LineSeries { + id: closableUpperSeries + } + LineSeries { + id: closableLowerSeries + XYPoint {x: dateTimeAxis.min.getTime(); y: 0} + XYPoint {x: dateTimeAxis.max.getTime(); y: 0} + } + + readonly property LogsModel logsModel: LogsModel { + id: logsModelNg + engine: typeIds.length ? _engine : null + thingId: thing ? thing.id : "" + typeIds: { + var ret = []; + ret.push(thing.thingClass.stateTypes.findByName("closed").id) + return ret; + } + live: true + viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + } + + BoolSeriesAdapter { + logsModel: closableDelegate.logsModel + xySeries: closableUpperSeries + inverted: true + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, boolAxis) + series.lowerSeries = closableLowerSeries + series.upperSeries = closableUpperSeries + series.color = Style.green + series.opacity = 0.1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + } + } + + Repeater { + model: zoneWrapper.thermostats.count + delegate: Item { + id: heatingDelegate + readonly property Thing thing: zoneWrapper.thermostats.get(index) + property AreaSeries series: null + + + LineSeries { + id: heatingUpperSeries + } + LineSeries { + id: heatingLowerSeries + XYPoint {x: dateTimeAxis.max.getTime(); y: 0} + XYPoint {x: dateTimeAxis.min.getTime(); y: 0} + + } + + readonly property LogsModel logsModel: LogsModel { + objectName: "heat: " + thing.name + engine: typeIds.length > 0 ? _engine : null + thingId: thing ? thing.id : "" + typeIds: { + var ret = []; + var heatingOnStateType = thing.thingClass.stateTypes.findByName("heatingOn") + print("**** has heatingOn") + if (heatingOnStateType) { + print("**** true") + ret.push(heatingOnStateType.id) + } + return ret; + } + live: true + // graphSeries: heatingUpperSeries + viewStartTime: dateTimeAxis.min + } + + BoolSeriesAdapter { + logsModel: heatingDelegate.logsModel + xySeries: heatingUpperSeries + } + + Component.onCompleted: { + series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, boolAxis) + series.lowerSeries = heatingLowerSeries + series.upperSeries = heatingUpperSeries + series.color = Style.red + series.opacity = 0.1 + // series.opacity = Qt.binding(function() { + // return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3 + // }) + + series.borderWidth = 0; + series.borderColor = series.color + } + Component.onDestruction: { + chartView.removeSeries(series) + } + } + } + } + + + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.leftMargin: chartView.plotArea.x + anchors.topMargin: chartView.plotArea.y + anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x + anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y + + hoverEnabled: true + preventStealing: tooltipping || dragging + + property int startMouseX: 0 + property bool dragging: false + property bool tooltipping: false + + property var startDatetime: null + + Timer { + interval: 300 + running: mouseArea.pressed + onTriggered: { + if (!mouseArea.dragging) { + mouseArea.tooltipping = true + } + } + } + onReleased: { + mouseArea.tooltipping = false; + + if (mouseArea.dragging) { + mouseArea.dragging = false; + } + } + + onPressed: { + startMouseX = mouseX + startDatetime = d.now + } + + + onDoubleClicked: { + if (selectionTabs.currentIndex == 0) { + return; + } + + var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width) + var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000)) + selectionTabs.currentIndex-- + d.now = new Date(Math.min(new Date().getTime(), timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000)) + powerBalanceLogs.fetchLogs() + logsLoader.fetchLogs() + } + + onMouseXChanged: { + if (!pressed || mouseArea.tooltipping) { + return; + } + if (Math.abs(startMouseX - mouseX) < 10) { + return; + } + dragging = true + + var dragDelta = startMouseX - mouseX + var totalTime = d.endTime.getTime() - d.startTime.getTime() + // dragDelta : timeDelta = width : totalTime + var timeDelta = dragDelta * totalTime / mouseArea.width + print("dragging", dragDelta, totalTime, mouseArea.width) + d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta))) + } + + onWheel: { + startDatetime = d.now + var totalTime = d.endTime.getTime() - d.startTime.getTime() + // pixelDelta : timeDelta = width : totalTime + var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width + print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width) + d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta))) + wheelStopTimer.restart() + } + Timer { + id: wheelStopTimer + interval: 300 + repeat: false + onTriggered: { + // for (var i = 0; i < consumersRepeater.count; i++) { + // if (consumersRepeater.itemAt(i).logs.fetchingData) { + // wheelStopTimer.start() + // return; + // } + // } + + // powerBalanceLogs.fetchLogs() + // logsLoader.fetchLogs() + } + } + + Rectangle { + height: parent.height + width: 1 + color: Style.foregroundColor + x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX)) + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + } + + Item { + id: tooltips + anchors.fill: parent + property var timestamp: new Date(((d.endTime.getTime() - d.startTime.getTime()) * mouseArea.mouseX / mouseArea.width) + d.startTime.getTime()) + property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins + property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - tooltipWidth + + property int tooltipWidth: 130 + property int tooltipX: xOnLeft < 0 ? xOnRight : xOnLeft + + onTimestampChanged: { + updateTimer.start(); + } + Timer { + id: updateTimer + interval: 0 + onTriggered: tooltips.update() + } + + function update() { + var ordered = [] + insert(thermostatTooltipRepeater, ordered); + insert(tempTooltipRepeater, ordered); + insert(humidityTooltipRepeater, ordered); + insert(vocTooltipRepeater, ordered); + + for (var i = ordered.length - 1; i >= 0; i--) { + var item = ordered[i] + var newY = item.realY + + if (i < ordered.length-1) { + var previous = ordered[i+1] + newY = Math.min(newY, previous.fixedY - item.height/* - Style.extraSmallMargins*/) + } + + ordered[i].fixedY = newY + } + } + + function insert(repeater, array) { + for (var i = 0; i < repeater.count; i++) { + var item = repeater.itemAt(i); + var insertIdx = 0; + while (array.length > insertIdx && item.realY > array[insertIdx].realY) { + insertIdx++ + } + array.splice(insertIdx, 0, item) + } + } + } + + + Repeater { + id: thermostatTooltipRepeater + model: thermostatsRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: thermostatsRepeater.itemAt(index).thing + entry: thermostatsRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("temperaturesensor") + axis: temperatureAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitDegreeCelsius + } + } + + Repeater { + id: tempTooltipRepeater + model: tempRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: tempRepeater.itemAt(index).thing + entry: tempRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("temperaturesensor") + axis: temperatureAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitDegreeCelsius + } + } + + Repeater { + id: humidityTooltipRepeater + model: humidityRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: humidityRepeater.itemAt(index).thing + entry: humidityRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("humiditysensor") + axis: humidityAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitPercentage + } + } + + Repeater { + id: vocTooltipRepeater + model: vocRepeater.count + + delegate: TooltipDelegate { + visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging + thing: vocRepeater.itemAt(index).thing + entry: vocRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + color: app.interfaceToColor("vocsensor") + axis: vocAxis + x: tooltips.tooltipX + width: tooltips.tooltipWidth + backgroundItem: chartView + backgroundRect: Qt.rect(mouseArea.x + x, mouseArea.y + y, width, height) + unit: Types.UnitPartsPerMillion + } + } + } + + + RowLayout { + id: legend + anchors { left: parent.left; bottom: parent.bottom; right: parent.right } + anchors.leftMargin: chartView.plotArea.x + height: Style.smallIconSize + anchors.margins: Style.margins + + Repeater { + model: thermostatsRepeater.count + delegate: LegendDelegate { + thing: thermostatsRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("thermostat") + color: app.interfaceToColor("temperaturesensor") + } + } + Repeater { + model: tempRepeater.count + delegate: LegendDelegate { + thing: tempRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("temperaturesensor") + color: app.interfaceToColor("temperaturesensor") + } + } + Repeater { + model: humidityRepeater.count + delegate: LegendDelegate { + thing: humidityRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("humiditysensor") + color: app.interfaceToColor("humiditysensor") + } + } + Repeater { + model: vocRepeater.count + delegate: LegendDelegate { + thing: vocRepeater.itemAt(index).thing + iconName: app.interfaceToIcon("vocsensor") + color: app.interfaceToColor("vocsensor") + } + } + } +} + diff --git a/nymea-app/ui/mainviews/airconditioning/ACSettingsPage.qml b/nymea-app/ui/mainviews/airconditioning/ACSettingsPage.qml new file mode 100644 index 00000000..3d933fbf --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ACSettingsPage.qml @@ -0,0 +1,73 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" +import "qrc:/ui/delegates" + +SettingsPageBase { + id: root + title: qsTr("Configure zones") + + property AirConditioningManager acManager: null + + header: NymeaHeader { + text: root.title + backButtonVisible: true + onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: "add" + onClicked: { + createZone(); + } + + } + } + + function createZone() { + pendingAddCall = acManager.addZone(qsTr("Zone %1").arg(acManager.zoneInfos.count + 1), [], [], [], []) + } + property int pendingAddCall: -1 + Connections { + target: acManager + + onAddZoneReply: { + if (commandId == pendingAddCall) { + print("zone added", zoneId) + var zone = acManager.zoneInfos.getZoneInfo(zoneId) + pageStack.push(Qt.resolvedUrl("EditZonePage.qml"), {acManager: acManager, zone: zone, createNew: true}) + } + } + } + + + Item { + width: parent.width + height: root.height - root.header.height + visible: acManager.zoneInfos.count == 0 + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + title: qsTr("No zones configured.") + text: qsTr("Start with configuring your zones.") + imageSource: "/ui/images/sensors.svg" + buttonText: qsTr("Add zone") + onButtonClicked: createZone() + } + } + + + Repeater { + model: acManager.zoneInfos + + delegate: NymeaItemDelegate { + property ZoneInfo zone: acManager.zoneInfos.get(index) + Layout.fillWidth: true + text: model.name + onClicked: pageStack.push(Qt.resolvedUrl("EditZonePage.qml"), {acManager: root.acManager, zone: zone}) + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/BigZoneStatusIcons.qml b/nymea-app/ui/mainviews/airconditioning/BigZoneStatusIcons.qml new file mode 100644 index 00000000..b56552cb --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/BigZoneStatusIcons.qml @@ -0,0 +1,162 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.3 +import "qrc:/ui/components" +import Nymea 1.0 +import NymeaApp.Utils 1.0 +import Nymea.AirConditioning 1.0 + +Item { + id: root + implicitHeight: layout.implicitHeight + implicitWidth: layout.implicitWidth + + property AirConditioningManager acManager: null + property ZoneInfoWrapper zoneWrapper: null + readonly property ZoneInfo zone: zoneWrapper ? zoneWrapper.zone : null + + property int iconSize: Style.iconSize + + signal clicked(int flag) + + property var zoneStatusModel: [ + { + value: ZoneInfo.ZoneStatusFlagSetpointOverrideActive, + icon: "dial", + color: Style.iconColor, + activeColor: Style.accentColor, + text: qsTr("Automatic mode"), + activeText: qsTr("Manual mode"), + visible: zoneWrapper.thermostats.count > 0, + alertVisible: false, + onClicked: function() { + var comp = Qt.createComponent(Qt.resolvedUrl("TimeOverrideDialog.qml")) + var dialog = comp.createObject(app, {acManager: root.acManager, zone: root.zone}) + dialog.open() + } + }, + { + value: ZoneInfo.ZoneStatusFlagTimeScheduleActive, + icon: "calendar", + color: Style.iconColor, + activeColor: Style.orange, + text: qsTr("Time schedule not active"), + activeText: qsTr("Time schedule active"), + visible: zoneWrapper.thermostats.count > 0, + alertVisible: false, + onClicked: function() { + pageStack.push(Qt.resolvedUrl("TimeSchedulePage.qml"), {acManager: root.acManager, zone: root.zone}) + } + }, + { + value: ZoneInfo.ZoneStatusFlagWindowOpen, + icon: "sensors/window-closed", + activeIcon: "sensors/window-open", + color: Style.green, + activeColor: Style.red, + text: qsTr("All windows closed"), + activeText: qsTr("%n window(s) open", "", zoneWrapper.openWindows.count), + visible: zoneWrapper.windowSensors.count > 0, + alertVisible: false + }, + { + value: ZoneInfo.ZoneStatusFlagNone, + icon: "sensors/temperature", + color: Style.iconColor, + activeColor: Style.accentColor, + text: Types.toUiValue(zoneWrapper.zoneTemperature.toFixed(1), Types.UnitDegreeCelsius) + Types.toUiUnit(Types.UnitDegreeCelsius), + visible: zoneWrapper.indoorTempSensors.count > 0 && zoneWrapper.thermostats.count == 0, + alertVisible: false + }, + { + value: ZoneInfo.ZoneStatusFlagHighHumidity, + icon: "sensors/humidity", + color: app.interfaceToColor("humiditysensor"), + activeColor: app.interfaceToColor("humiditysensor"), + text: qsTr("%1% humidity").arg(zoneWrapper.zoneHumidity.toFixed(0)), + activeText:qsTr("%1% humidity").arg(zoneWrapper.zoneHumidity.toFixed(0)), + visible: zoneWrapper.indoorHumiditySensors.count > 0, + alertVisible: (root.zone.zoneStatus & ZoneInfo.ZoneStatusFlagHighHumidity) > 0 + }, + { + value: ZoneInfo.ZoneStatusFlagBadAir, + icon: "weathericons/weather-clouds", + color: AirQualityIndex.currentIndex(AirQualityIndex.iaqVoc, zoneWrapper.zoneVOC).color, + activeColor: AirQualityIndex.currentIndex(AirQualityIndex.iaqVoc, zoneWrapper.zoneVOC).color, + text: AirQualityIndex.currentIndex(AirQualityIndex.iaqVoc, zoneWrapper.zoneVOC).text, + activeText: AirQualityIndex.currentIndex(AirQualityIndex.iaqVoc, zoneWrapper.zoneVOC).text,// qsTr("Air quality alert!"), + visible: zoneWrapper.indoorVocSensors.count > 0 || zoneWrapper.indoorPm25Sensors.count > 0, + alertVisible: (root.zone.zoneStatus & ZoneInfo.ZoneStatusFlagBadAir) > 0 + } + ] + + GridLayout { + id: layout + flow: GridLayout.TopToBottom + rows: { + var ret = 0; + for (var i = 0; i < zoneStatusModel.length; i++) { + var entry = zoneStatusModel[i] + if (entry.visible) { + ret++ + } + } + return ret; + } + anchors.fill: parent + columnSpacing: Style.smallMargins + rowSpacing: Style.smallMargins + + Repeater { + model: zoneStatusModel + delegate: Item { + Layout.fillWidth: false + implicitHeight: root.iconSize + implicitWidth: root.width / 3 + visible: entry.visible + + property var entry: zoneStatusModel[index] + property bool active: (root.zone.zoneStatus & entry.value) > 0 + ColorIcon { + name: entry.hasOwnProperty("activeIcon") && active ? entry.activeIcon : entry.icon + size: root.iconSize + color: active ? entry.activeColor : Style.iconColor + anchors.right: parent.right + } + + MouseArea { + anchors.fill: parent + onClicked: entry.onClicked() + } + } + } + Repeater { + model: zoneStatusModel + + delegate: RowLayout { + Layout.fillWidth: true + implicitHeight: root.iconSize + implicitWidth: 100 + visible: entry.visible + + property var entry: zoneStatusModel[index] + property bool active: (root.zone.zoneStatus & entry.value) > 0 + + Label { +// Layout.alignment: Qt.AlignVCenter + text: active ? entry.activeText : entry.text + elide: Text.ElideRight + } + ColorIcon { + size: Style.iconSize + name: "attention" + color: Style.yellow + visible: entry.alertVisible + } + Item { + Layout.fillWidth: true + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/CreateZoneWizard.qml b/nymea-app/ui/mainviews/airconditioning/CreateZoneWizard.qml new file mode 100644 index 00000000..9c00da8e --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/CreateZoneWizard.qml @@ -0,0 +1,143 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 + +WizardPageBase { + id: root + property AirConditioningManager acManager: null + title: qsTr("New zone") + + showBackButton: true + showExtraButton: false + + + QtObject { + id: d + property var thermostats: [] + property var windowSensors: [] + property var indoorSensors: [] + property var outdoorSensors: [] + } + + onBack: pageStack.pop(); + + onNext: { + acManager.addZone(nameTextField.text, d.thermostats, d.windowSensors, d.indoorSensors, d.outdoorSensors) + pageStack.pop(); + } + + ThingsProxy { + id: thermostatsProxy + engine: _engine + shownInterfaces: ["thermostat"] + } + ThingsProxy { + id: windowSensorsProxy + engine: _engine + shownInterfaces: ["closablesensors"] + } + + ThingsProxy { + id: sensorsProxy + engine: _engine + shownInterfaces: ["temperaturesensor", "humiditysensor", "vocsensor", "pm25sensor"] + hiddenInterfaces: ["thermostat"] + } + + content: Item { + Layout.fillWidth: true + Layout.preferredHeight: root.visibleContentHeight + + Flickable { + id: flickable + anchors.fill: parent + contentHeight: contentColumn.height + + + ColumnLayout { + id: contentColumn + width: flickable.width + + SettingsPageSectionHeader { + text: qsTr("Zone name") + } + + NymeaTextField { + id: nameTextField + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + } + + + Label { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("Select the thermostats that should be part of this zone.") + wrapMode: Text.WordWrap + } + + Repeater { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + model: thermostatsProxy + + delegate: CheckDelegate { + Layout.fillWidth: true + text: model.name + checked: d.thermostats.indexOf(model.id) >= 0 + onClicked: { + var tmp = d.thermostats + if (checked) { + tmp.push(model.id) + } else { + var idx = tmp.indexOf(model.id); + tmp.splice(idx, 1) + } + d.thermostats = tmp; + } + } + } + + Label { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("Select the sensors that should be part of this zone.") + wrapMode: Text.WordWrap + } + + Repeater { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + model: sensorsProxy + + delegate: CheckDelegate { + Layout.fillWidth: true + text: model.name + checked: d.things.indexOf(model.id) >= 0 + onClicked: { + var tmp = d.sensors + if (checked) { + tmp.push(model.id) + } else { + var idx = tmp.indexOf(model.id); + tmp.splice(idx, 1) + } + d.sensors = tmp; + } + } + } + } + } + + } + +} diff --git a/nymea-app/ui/mainviews/airconditioning/EditZonePage.qml b/nymea-app/ui/mainviews/airconditioning/EditZonePage.qml new file mode 100644 index 00000000..901343e6 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/EditZonePage.qml @@ -0,0 +1,153 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" +import "qrc:/ui/delegates" + +SettingsPageBase { + id: editZonePage + + title: qsTr("Edit %1").arg(zone.name) + + property AirConditioningManager acManager: null + property ZoneInfo zone: null + property bool createNew: false + + busy: d.pendingCommandId != -1 + QtObject { + id: d + property int pendingCommandId: -1 + } + + Connections { + target: acManager + onSetZoneNameReply: { + if (commandId == d.pendingCommandId) { + d.pendingCommandId = -1 + } + } + + onRemoveZoneReply: { + if (commandId == d.pendingCommandId) { + d.pendingCommandId = -1 + pageStack.pop() + } + } + } + + SettingsPageSectionHeader { + text: qsTr("Zone information") + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: app.margins + Layout.rightMargin: app.margins + spacing: app.margins + + Label { + text: qsTr("Name") + Layout.fillWidth: true + } + TextField { + id: nameTextField + Layout.fillWidth: true + text: zone.name + } + Button { + text: qsTr("OK") + visible: nameTextField.displayText !== zone.name + onClicked: d.pendingCommandId = acManager.setZoneName(zone.id, nameTextField.displayText) + } + } + + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Assigned things") + onClicked: pageStack.push(Qt.resolvedUrl("EditZoneThingsPage.qml"), {acManager: acManager, zone: zone}) + } + + SettingsPageSectionHeader { + text: qsTr("Temperature settings") + } + + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Base temperature") + subText: Types.toUiValue(editZonePage.zone.standbySetpoint, Types.UnitDegreeCelsius) + Types.toUiUnit(Types.UnitDegreeCelsius) + onClicked: { + var popup = selectBaseTempComponent.createObject(app, {zone: zone}) + popup.open() + } + } + + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Set time schedule") + onClicked: { + pageStack.push(Qt.resolvedUrl("TimeSchedulePage.qml"), {acManager: acManager, zone: zone}) + } + } + + +// SettingsPageSectionHeader { +// text: qsTr("Notification settings") +// } + +// SwitchDelegate { +// Layout.fillWidth: true +// text: qsTr("Bad air") +// } +// SwitchDelegate { +// Layout.fillWidth: true +// text: qsTr("Humidity") +// } + + Button { + Layout.fillWidth: true + Layout.margins: Style.margins + text: createNew ? qsTr("OK") : qsTr("Remove this zone") + onClicked: { + if (createNew) { + pageStack.pop() + } else { + d.pendingCommandId = acManager.removeZone(zone.id) + } + } + } + + Component { + id: selectBaseTempComponent + NymeaDialog { + id: selectBaseTempDialog + + property ZoneInfo zone: null + + CircleBackground { + Layout.fillWidth: true + Layout.preferredHeight: width + + Dial { + anchors.fill: parent + value: selectBaseTempDialog.zone.standbySetpoint + precision: 1 + minValue: 10 + maxValue: 40 + + onMoved: { + acManager.setZoneStandbySetpoint(zone.id, value) + + } + } + + Label { + anchors.centerIn: parent + text: Types.toUiValue(zone.standbySetpoint, Types.UnitDegreeCelsius) + Types.toUiUnit(Types.UnitDegreeCelsius) + font: Style.bigFont + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/EditZoneThingsPage.qml b/nymea-app/ui/mainviews/airconditioning/EditZoneThingsPage.qml new file mode 100644 index 00000000..8138ca73 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/EditZoneThingsPage.qml @@ -0,0 +1,261 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" +import "qrc:/ui/delegates" + +SettingsPageBase { + id: zoneThingsPage + title: qsTr("Things in zone %1").arg(zone.name) + + property AirConditioningManager acManager: null + property ZoneInfo zone: null + + ZoneInfoWrapper { + id: zoneWrapper + zone: zoneThingsPage.zone + } + + busy: d.pendingCommandId != -1 + QtObject { + id: d + property int pendingCommandId: -1 + } + + Connections { + target: acManager + onSetZoneNameReply: { + if (commandId == d.pendingCommandId) { + d.pendingCommandId = -1 + } + } + } + + SettingsPageSectionHeader { + text: qsTr("Thermostats") + } + + Repeater { + model: zoneWrapper.thermostats + delegate: ThingDelegate { + Layout.fillWidth: true + thing: zoneWrapper.thermostats.get(index) + progressive: false + canDelete: true + onDeleteClicked: { + acManager.removeZoneThermostat(zone.id, thing.id) + } + } + } + Button { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("Add thermostat") + onClicked: { + var page = pageStack.push(selectThingComponent, { + acManager: acManager, + zone: zone, + interfaces: ["thermostat"], + hiddenThingIds: zone.thermostats, + title: qsTr("Add thermostats"), + placeHolderTitle: qsTr("No thermostats installed"), + placeHolderText: qsTr("Before a thermostat can be assigned to this zone, it needs to be connected to nymea."), + placeHolderButtonText: qsTr("Setup thermostats"), + placeHolderFilterInterface: "thermostat" + }) + page.selected.connect(function(thingId) { + acManager.addZoneThermostat(zone.id, thingId) + }) + } + } + + SettingsPageSectionHeader { + text: qsTr("Window sensors") + } + + Repeater { + model: zoneWrapper.windowSensors + delegate: ThingDelegate { + Layout.fillWidth: true + thing: zoneWrapper.windowSensors.get(index) + progressive: false + canDelete: true + onDeleteClicked: { + acManager.removeZoneWindowSensor(zone.id, thing.id) + } + } + } + + Button { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("Add window sensor") + onClicked: { + var page = pageStack.push(selectThingComponent, { + acManager: acManager, + zone: zone, + interfaces: ["closablesensor"], + hiddenThingIds: zone.windowSensors, + title: qsTr("Add window sensors"), + placeHolderTitle: qsTr("No window sensors installed"), + placeHolderText: qsTr("Before a window sensor can be assigned to this zone, it needs to be connected to nymea."), + placeHolderButtonText: qsTr("Setup window sensor"), + placeHolderFilterInterface: "closablesensor" + }) + page.selected.connect(function(thingId) { + acManager.addZoneWindowSensor(zone.id, thingId) + }) + } + } + SettingsPageSectionHeader { + text: qsTr("Indoor sensors") + } + + Repeater { + model: zoneWrapper.indoorSensors + delegate: ThingDelegate { + Layout.fillWidth: true + thing: zoneWrapper.indoorSensors.get(index) + progressive: false + canDelete: true + onDeleteClicked: { + acManager.removeZoneIndoorSensor(zone.id, thing.id) + } + } + } + + Button { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("Add indoor sensor") + onClicked: { + var page = pageStack.push(selectThingComponent, { + acManager: acManager, + zone: zone, + interfaces: ["temperaturesensor", "humiditysensor", "vocsensor", "pm25sensor"], + hiddenThingIds: zone.indoorSensors, + title: qsTr("Add indoor sensors"), + placeHolderTitle: qsTr("No sensors installed"), + placeHolderText: qsTr("Before a sensor be assigned to this zone, it needs to be connected to nymea."), + placeHolderButtonText: qsTr("Setup sensors"), + placeHolderFilterInterface: "sensor" + }) + page.selected.connect(function(thingId) { + acManager.addZoneIndoorSensor(zone.id, thingId) + }) + } + } + + SettingsPageSectionHeader { + text: qsTr("Outdoor sensors") + } + + Repeater { + model: zoneWrapper.outdoorSensors + delegate: ThingDelegate { + Layout.fillWidth: true + thing: zoneWrapper.outdoorSensors.get(index) + progressive: false + canDelete: true + onDeleteClicked: { + acManager.removeZoneOutdoorSensor(zone.id, thing.id) + } + } + } + + Button { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("Add outdoor sensor") + onClicked: { + var page = pageStack.push(selectThingComponent, { + acManager: acManager, + zone: zone, + interfaces: ["temperaturesensor", "humiditysensor", "vocsensor", "pm25sensor"], + hiddenThingIds: zone.outdoorSensors, + title: qsTr("Select outdoor sensors"), + placeHolderTitle: qsTr("No sensors installed"), + placeHolderText: qsTr("Before a sensor be assigned to this zone, it needs to be connected to nymea."), + placeHolderButtonText: qsTr("Setup sensors"), + placeHolderFilterInterface: "sensor" + }) + page.selected.connect(function(thingId) { + acManager.addZoneOutdoorSensor(zone.id, thingId) + }) + } + } + + Component { + id: selectThingComponent + SettingsPageBase { + id: selectThingPage + busy: d.pendingCommandId != -1 + + property AirConditioningManager acManager: null + property ZoneInfo zone: null + property var interfaces: [] + property var hiddenThingIds: [] + + property alias placeHolderTitle: placeHolder.title + property alias placeHolderText: placeHolder.text + property alias placeHolderButtonText: placeHolder.buttonText + property string placeHolderFilterInterface: "" + + signal selected(var thingId) + + QtObject { + id: d + property int pendingCommandId: -1 + } + + Connections { + target: acManager + onSetZoneThingsReply: { + if (commandId == d.pendingCommandId) { + d.pendingCommandId = -1 + } + + pageStack.pop(); + } + } + + Repeater { + + model: ThingsProxy { + id: thingsProxy + engine: _engine + shownInterfaces: selectThingPage.interfaces + hiddenThingIds: selectThingPage.hiddenThingIds + } + + delegate: ThingDelegate { + Layout.fillWidth: true + thing: thingsProxy.get(index) + progressive: false + onClicked: selectThingPage.selected(thing.id) + } + } + + Item { + visible: thingsProxy.count == 0 + width: selectThingPage.width + height: selectThingPage.height - selectThingPage.header.height + + EmptyViewPlaceholder { + id: placeHolder + anchors.centerIn: parent + width: parent.width - app.margins * 2 + imageSource: app.interfaceToIcon(selectThingPage.placeHolderFilterInterface) + buttonText: qsTr("Add things") + onButtonClicked: { + pageStack.push("/ui/thingconfiguration/NewThingPage.qml", {filterInterface: selectThingPage.placeHolderFilterInterface}) + } + } + } + } + } +} + + diff --git a/nymea-app/ui/mainviews/airconditioning/LegendDelegate.qml b/nymea-app/ui/mainviews/airconditioning/LegendDelegate.qml new file mode 100644 index 00000000..6cb8e566 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/LegendDelegate.qml @@ -0,0 +1,42 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import QtCharts 2.3 + +Item { + id: root + property Thing thing: null + property string iconName: "" + property color color: "white" + Layout.fillWidth: true + Layout.fillHeight: true + // opacity: selfProductionConsumptionSeries.opacity + MouseArea { + anchors.fill: parent + anchors.topMargin: -Style.smallMargins + anchors.bottomMargin: -Style.smallMargins + // onClicked: d.selectSeries(selfProductionConsumptionSeries) + } + Row { + anchors.centerIn: parent + spacing: Style.smallMargins + ColorIcon { + name: root.iconName + size: Style.smallIconSize + color: root.color + } + Label { + width: parent.parent.width - x + elide: Text.ElideRight + visible: root.width > 60 + text: root.thing.name + anchors.verticalCenter: parent.verticalCenter + font: Style.smallFont + } + } +} + diff --git a/nymea-app/ui/mainviews/airconditioning/SelectSensorsPage.qml b/nymea-app/ui/mainviews/airconditioning/SelectSensorsPage.qml new file mode 100644 index 00000000..47656a98 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/SelectSensorsPage.qml @@ -0,0 +1,73 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 + +Page { + id: root + property AirConditioningManager acManager: null + property ZoneInfo zoneInfo: null + + readonly property Thing thermostat: engine.thingManager.things.getThing(root.zoneInfo.thermostatId) + + header: NymeaHeader { + text: root.thermostat.name + + onBackPressed: { + pageStack.pop() + } + + HeaderButton { + imageSource: "tick" + onClicked: { + var sensorIds = [] + acManager.setZoneThings(root.zoneInfo.id, d.checkedThings) + } + } + } + + QtObject { + id: d + property var checkedThings: root.zoneInfo.thingIds + + } + + Component.onCompleted: print("***** sensors", root.zoneInfo.thingIds, d.checkedThings) + + GroupedListView { + id: sensorsListView + anchors.fill: parent + + section.property: "mainInterface" + model: ThingsProxy { + id: sensorsProxy + engine: _engine + shownInterfaces: ["thermostat", "closablesensor", "temperaturesensor", "humiditysensor", "vocsensor", "pm25sensor"] +// hiddenInterfaces: ["thermostat"] + groupByInterface: true + } + delegate: CheckDelegate { + readonly property Thing thing: sensorsProxy.get(index) + width: parent.width + text: model.name + checked: { + for (var i = 0; i < d.checkedThings.length; i++) { + if (d.checkedThings[i] == model.id) { // Intentionally + return true; + } + } + return false; + } + onClicked: { + if (checked) { + d.checkedThings.push(model.id) + } else { + d.checkedThings.splice(d.checkedThings.indexOf(model.id.toString()), 1) + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/TemperatureScheduleEditor.qml b/nymea-app/ui/mainviews/airconditioning/TemperatureScheduleEditor.qml new file mode 100644 index 00000000..ab34980a --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/TemperatureScheduleEditor.qml @@ -0,0 +1,406 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" + +ColumnLayout { + id: root + Layout.minimumHeight: 100 + property string title: "" + + property ZoneInfo zone: null + property TemperatureDaySchedule daySchedule: null + + property TemperatureDaySchedule scheduleClipboard: null + + signal copyClicked() + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: root.title + } + + ProgressButton { + imageSource: paste ? "edit-paste" : "edit-copy" + color: root.scheduleClipboard == root.daySchedule ? Style.accentColor : Style.iconColor + property bool paste: root.scheduleClipboard != null && root.scheduleClipboard != root.daySchedule + onClicked: { + if (paste) { + daySchedule.clear(); + for (var i = 0; i < root.scheduleClipboard.count; i++) { + var schedule = root.scheduleClipboard.get(i) + daySchedule.createSchedule(schedule.startTime, schedule.endTime, schedule.temperature) + } + } else { + root.copyClicked() + } + } + } + } + + + QtObject { + id: d + property var freeBlocks: { + var ret = [] + for (var i = 0; i < daySchedule.count; i++) { + var previous = i == 0 ? null : daySchedule.get(i - 1) + var previousMins = previous ? previous.endTime.getHours() * 60 + previous.endTime.getMinutes() : 0 + var schedule = daySchedule.get(i) + var startMins = schedule.startTime.getHours() * 60 + schedule.startTime.getMinutes() + var endMins = schedule.endTime.getHours() * 60 + schedule.endTime.getMinutes() + + if (startMins > previousMins + 60 * 3) { + ret.push({startMins: previousMins, endMins: startMins}) + } + } + var last = daySchedule.count > 0 ? daySchedule.get(daySchedule.count - 1) : null + var lastMins = last ? last.endTime.getHours() * 60 + last.endTime.getMinutes() : 0 + if (lastMins < (21*60)) { + ret.push({startMins: lastMins, endMins: 24*60}) + } + + return ret + } + } + + Rectangle { + id: slider + Layout.fillWidth: true + Layout.fillHeight: true + color: Style.tileBackgroundColor + radius: Style.cornerRadius + clip: true + + + Repeater { + model: 24 + delegate: Rectangle { + height: parent.height + width: 1 + color: Style.tileOverlayColor + x: slider.width / 24 * index + visible: index > 0 + } + } + + Repeater { + model: root.daySchedule + delegate: Rectangle { + id: blockDelegate + readonly property TemperatureSchedule schedule: root.daySchedule.get(index) + readonly property int startMinutes: schedule.startTime.getHours() * 60 + schedule.startTime.getMinutes() + readonly property int endMinutes: schedule.endTime.getHours() * 60 + schedule.endTime.getMinutes() + readonly property int totalMinutes: 24 * 60 + height: slider.height + width: endMinutes * slider.width / totalMinutes - x + x: startMinutes * slider.width / totalMinutes + radius: Style.cornerRadius + color: schedule.temperature >= zone.standbySetpoint ? Style.red : Style.blue + + Rectangle { + anchors { + left: parent.left; + leftMargin: Style.extraSmallMargins + verticalCenter: parent.verticalCenter + } + height: Style.font.pixelSize + width: 2 + radius: width / 2 + color: Style.white + } + Rectangle { + anchors { + right: parent.right + rightMargin: Style.extraSmallMargins + verticalCenter: parent.verticalCenter + } + height: Style.font.pixelSize + width: 2 + radius: width / 2 + color: Style.white + } + + Label { + anchors { left: parent.left; right: parent.right; margins: Style.extraSmallMargins; top: parent.top } + anchors.leftMargin: parent.width >= implicitWidth + Style.smallMargins ? Style.extraSmallMargins : -(implicitWidth + Style.extraSmallMargins) + horizontalAlignment: Text.AlignLeft + font: Style.extraSmallFont + text: blockDelegate.schedule.startTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + } + + Label { + anchors { left: parent.left; right: parent.right; margins: Style.smallMargins; verticalCenter: parent.verticalCenter } + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + text: Types.toUiValue(blockDelegate.schedule.temperature, Types.UnitDegreeCelsius) + "°" + elide: Text.ElideRight + } + Label { + anchors { left: parent.left; right: parent.right; margins: Style.extraSmallMargins; bottom: parent.bottom } + anchors.rightMargin: parent.width >= implicitWidth + Style.smallMargins ? Style.extraSmallMargins : -(implicitWidth + Style.extraSmallMargins) + horizontalAlignment: Text.AlignRight + font: Style.extraSmallFont + text: blockDelegate.schedule.endTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) +// elide: Text.ElideRight + } + + } + } + + + + MouseArea { + anchors.fill: parent + + property bool moveStart: false + property bool moveEnd: false + property TemperatureSchedule previousSchedule: null + property TemperatureSchedule movedSchedule: null + property TemperatureSchedule nextSchedule: null + + property int startMouseX: 0 + property int startMins: 0 + property int endMins: 0 + + onPressed: { + movedSchedule = null + startMouseX = mouseX + var totalMins = 24 * 60 + for (var i = 0; i < root.daySchedule.count; i++) { + var schedule = root.daySchedule.get(i) + print("schedule:", schedule.startTime, schedule.endTime) + var startMin = schedule.startTime.getHours() * 60 + schedule.startTime.getMinutes() + var startPos = startMin * slider.width / totalMins + var endMin = schedule.endTime.getHours() * 60 + schedule.endTime.getMinutes() + var endPos = endMin * slider.width / totalMins + + if (Math.abs(startPos - mouseX) < 10) { + moveStart = true; + print("start") + } else if (Math.abs(endPos - mouseX) < 10) { + moveEnd = true + print("end") + } else if (mouseX > startPos && mouseX < endPos) { + moveStart = true + moveEnd = true + print("middle") + } else { + continue + } + startMins = startMin + endMins = endMin + previousSchedule = i > 0 ? root.daySchedule.get(i-1) : null + movedSchedule = schedule + nextSchedule = i < root.daySchedule.count - 1 ? root.daySchedule.get(i+1) : null + break; + } + } + + onReleased: { + moveStart = false; + moveEnd = false; + preventStealing = false; + } + + onClicked: { + print("clicked") + if (movedSchedule != null && Math.abs(mouseX - startMouseX) < 5) { + print("opening") + var dialog = editDialogComponent.createObject(root, {schedule: movedSchedule}) + dialog.open() + + } + } + + onPositionChanged: { + var totalMins = 24 * 60 + var diffX = mouseX - startMouseX + + // dY : height = dM : total + var diffMins = diffX * totalMins / slider.width + print("diffX", diffX, "diffMins", diffMins, startMins, startMins + diffMins) + + var newStart = new Date(movedSchedule.startTime); + var newEnd = new Date(movedSchedule.endTime); + + var newStartMins = startMins + (moveStart ? diffMins : 0); + var newEndMins = endMins + (moveEnd ? diffMins : 0); + + var snapMinutes = 30 + + var leftLimit = previousSchedule ? previousSchedule.endTime.getHours() * 60 + previousSchedule.endTime.getMinutes() + snapMinutes : 0 + var rightLimit = nextSchedule ? nextSchedule.startTime.getHours() * 60 + nextSchedule.startTime.getMinutes() - snapMinutes : totalMins + + + if (moveStart && !moveEnd) { + newStartMins = Math.max(leftLimit, newStartMins) + newStartMins = Math.min(newEndMins - 60, newStartMins) + } else if (moveEnd && !moveStart) { + newEndMins = Math.min(rightLimit, newEndMins) + newEndMins = Math.max(newStartMins + 60, newEndMins) + } else if (moveStart && moveEnd) { + newStartMins = Math.max(leftLimit, newStartMins) + newEndMins = Math.min(rightLimit, newEndMins) + + var blockSize = endMins - startMins + + newStartMins = Math.max(leftLimit, Math.min(newEndMins - blockSize, newStartMins)) + newEndMins = Math.max(newStartMins + blockSize, newEndMins) + + } + + var startSnapOffset = newStartMins % snapMinutes + if (startSnapOffset < snapMinutes / 2) { + newStartMins -= startSnapOffset + } else { + newStartMins += (snapMinutes - startSnapOffset) + } + var endSnapOffset = newEndMins % snapMinutes + if (endSnapOffset < snapMinutes / 2) { + newEndMins -= endSnapOffset + } else { + newEndMins += (snapMinutes - endSnapOffset) + } + print("startSnapOffset", startSnapOffset, "endSnapOff", endSnapOffset, "nes start", newStartMins, newEndMins) + + if (newEndMins == totalMins) { + newEndMins -= 1 + } + + newStart.setHours(0, newStartMins) + newEnd.setHours(0, newEndMins) + + if (movedSchedule.startTime.getTime() !== newStart.getTime() || movedSchedule.endTime.getTime() !== newEnd.getTime()) { + preventStealing = true; + } + + movedSchedule.startTime = newStart + movedSchedule.endTime = newEnd + + print("start time is new", newStart.toLocaleTimeString()) + print("end time is new", newEnd.toLocaleTimeString()) + + } + } + + Repeater { + model: d.freeBlocks + delegate: Item { + id: freeBlockDelegate + property var block: d.freeBlocks[index] + x: block.startMins * slider.width / (24*60) + width: block.endMins * slider.width / (24*60) - x + height: slider.height + + ProgressButton { + anchors.centerIn: parent + imageSource: "add" + onClicked: { + var startTime = new Date() + var endTime = new Date() + if (root.daySchedule.count == 0) { + startTime.setHours(6, 0, 0) + endTime.setHours(18, 0, 0) + } else { + startTime.setHours(0, freeBlockDelegate.block.startMins + 60, 0) + endTime.setHours(0, freeBlockDelegate.block.endMins - 60, 0) + } + + root.daySchedule.createSchedule(startTime, endTime, 21) + } + } + } + } + } + + Component { + id: editDialogComponent + NymeaDialog { + id: editDialog + x: (parent.width - width) / 2 + property TemperatureSchedule schedule: null + + standardButtons: Dialog.NoButton + + Dial { + id: dial + Layout.fillWidth: true + Layout.preferredHeight: width + activeValue: root.zone.standbySetpoint + minValue: 10 + maxValue: 30 + precision: 0.5 + value: editDialog.schedule.temperature + onMoved: editDialog.schedule.temperature = value + color: activeValue <= value ? Style.red : Style.blue + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: -Style.smallMargins + width: parent.contentItem.width * 0.6 + Label { + Layout.fillWidth: true + text: Types.toUiUnit(Types.UnitDegreeCelsius) + font.pixelSize: Math.min(Style.smallFont.pixelSize, dial.height / 16) + horizontalAlignment: Text.AlignHCenter + } + Label { + Layout.fillWidth: true + text: Types.toUiValue(editDialog.schedule.temperature, Types.UnitDegreeCelsius).toFixed(1) + font.pixelSize: Math.min(Style.hugeFont.pixelSize, dial.height / 8) + horizontalAlignment: Text.AlignHCenter + color: zone.currentSetpoint > zone.standbySetpoint + ? Style.red + : zone.currentSetpoint < zone.standbySetpoint + ? Style.blue + : Style.foregroundColor + } + Label { + Layout.fillWidth: true + text: Types.toUiValue(zone.standbySetpoint, Types.UnitDegreeCelsius).toFixed(1) + font.pixelSize: Math.min(Style.largeFont.pixelSize, dial.height / 12) + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + + + } + + + } + + Label { + Layout.fillWidth: true + text: editDialog.schedule.startTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + + " - " + + editDialog.schedule.endTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + horizontalAlignment: Text.AlignHCenter + } + + RowLayout { + Button { + text: qsTr("Remove") + Layout.fillWidth: true + onClicked: { + root.daySchedule.removeSchedule(editDialog.schedule) + editDialog.close() + } + } + Button { + text: qsTr("OK") + Layout.fillWidth: true + onClicked: { + editDialog.close() + } + } + } + + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/TimeOverrideDialog.qml b/nymea-app/ui/mainviews/airconditioning/TimeOverrideDialog.qml new file mode 100644 index 00000000..de36fb2a --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/TimeOverrideDialog.qml @@ -0,0 +1,130 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import "qrc:/ui/delegates" +import Nymea 1.0 +import NymeaApp.Utils 1.0 +import Nymea.AirConditioning 1.0 + +NymeaDialog { + id: root + standardButtons: Dialog.NoButton + + title: qsTr("Manual mode") + text: qsTr("Select how long the manual temperature setpoint should be kept.") + + property AirConditioningManager acManager: null + property ZoneInfo zone: null + + RadioButton { + id: eventualButton + Layout.fillWidth: true + text: qsTr("Eventual") + checked: root.zone.setpointOverrideMode == ZoneInfo.SetpointOverrideModeEventual + contentItem: ColumnLayout { + width: root.width + Label { + Layout.fillWidth: true + Layout.leftMargin: eventualButton.indicator.width + eventualButton.spacing + text: eventualButton.text + } + Label { + Layout.fillWidth: true + Layout.leftMargin: eventualButton.indicator.width + eventualButton.spacing + wrapMode: Text.WordWrap + text: qsTr("Until the temperature is changed by some other event.") + font: Style.smallFont + } + } + } + + RadioButton { + id: foreverButton + Layout.fillWidth: true + text: qsTr("Forever") + checked: root.zone.setpointOverrideMode == ZoneInfo.SetpointOverrideModeUnlimited + contentItem: ColumnLayout { + width: root.width + Label { + Layout.fillWidth: true + Layout.leftMargin: foreverButton.indicator.width + foreverButton.spacing + text: foreverButton.text + } + Label { + Layout.fillWidth: true + Layout.leftMargin: foreverButton.indicator.width + foreverButton.spacing + wrapMode: Text.WordWrap + text: qsTr("Until manually removed or changed.") + font: Style.smallFont + } + } + } + RadioButton { + id: timeButton + text: qsTr("Time") + checked: root.zone.setpointOverrideMode == ZoneInfo.SetpointOverrideModeTimed + contentItem: ColumnLayout { + width: root.width + Label { + Layout.fillWidth: true + Layout.leftMargin: timeButton.indicator.width + timeButton.spacing + text: timeButton.text + } + Label { + Layout.fillWidth: true + Layout.leftMargin: timeButton.indicator.width + timeButton.spacing + wrapMode: Text.WordWrap + text: qsTr("For a specified amount of time.") + font: Style.smallFont + } + } + } + + RowLayout { + Layout.leftMargin: timeButton.indicator.width + timeButton.spacing + enabled: timeButton.checked + SpinBox { + from: 30 + to: 30 * 2 * 12 + stepSize: 30 + value: 120 + textFromValue: function(value) { + return Math.floor(value / 60) + ":" + NymeaUtils.pad(value % 60, 2) + } + } + } + + + RowLayout { + Layout.fillWidth: true + Button { + text: qsTr("Remove") + onClicked: { + acManager.setZoneSetpointOverride(root.zone.id, root.zone.setpointOverride, ZoneInfo.SetpointOverrideModeNone, 0) + root.close(); + } + } + Item { + Layout.fillWidth: true + } + + Button { + text: qsTr("OK") + onClicked: { + var mode = ZoneInfo.SetpointOverrideModeEventual + if (foreverButton.checked) { + mode = ZoneInfo.SetpointOverrideModeUnlimited + } else if (timeButton.checked) { + mode = ZoneInfo.SetpointOverrideModeTimed + } + acManager.setZoneSetpointOverride(root.zone.id, root.zone.setpointOverride, mode, 120) + + root.close(); + } + } + } + +} + diff --git a/nymea-app/ui/mainviews/airconditioning/TimeSchedulePage.qml b/nymea-app/ui/mainviews/airconditioning/TimeSchedulePage.qml new file mode 100644 index 00000000..3df15573 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/TimeSchedulePage.qml @@ -0,0 +1,263 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 + +Page { + id: root + property AirConditioningManager acManager: null + property ZoneInfo zone: null + + readonly property TemperatureWeekSchedule weekSchedule: zone.weekSchedule.clone() + + + header: NymeaHeader { + text: root.zone.name + + onBackPressed: { + pageStack.pop() + } + + HeaderButton { + imageSource: "tick" + onClicked: { + acManager.setZoneWeekSchedule(root.zone.id, root.weekSchedule) + pageStack.pop(); + } + } + } + + Flickable { + id: flickable + anchors.fill: parent + contentWidth: editorItem.width + contentHeight: editorItem.height + clip: true + + Item { + id: editorItem + width: flickable.width + height: childrenRect.height + Style.margins * 2 + + ColumnLayout { + anchors { left: parent.left; top: parent.top; right: parent.right; margins: Style.margins } + +// RowLayout { +// Label { +// text: qsTr("Base temperature: %1 %2").arg(Types.toUiValue(zone.standbySetpoint, Types.UnitDegreeCelsius)).arg(Types.toUiUnit(Types.UnitDegreeCelsius)) +// } +// } + + Repeater { + id: scheduleRepeater + model: [ + qsTr("Monday"), + qsTr("Tuesday"), + qsTr("Wednesday"), + qsTr("Thursday"), + qsTr("Friday"), + qsTr("Saturday"), + qsTr("Sunday") + ] + property TemperatureDaySchedule scheduleClipboard: null + + delegate: TemperatureScheduleEditor { + title: modelData + Layout.fillHeight: true + Layout.fillWidth: true + zone: root.zone + daySchedule: weekSchedule.get(index) + scheduleClipboard: scheduleRepeater.scheduleClipboard + onCopyClicked: { + if (scheduleRepeater.scheduleClipboard == daySchedule) { + scheduleRepeater.scheduleClipboard = null + } else { + scheduleRepeater.scheduleClipboard = daySchedule + } + } + } + } + + // CheckBox { + // text: qsTr("Use sunday schedule for public holidays.") + // } + } + } + + } + + + + +// Item { +// id: scheduleEditor +// anchors.fill: parent +// anchors.margins: Style.margins + +// Rectangle { +// id: slider +// anchors { +// top: parent.top +// left: parent.left +// bottom: parent.bottom +// } +// width: Style.largeDelegateHeight +// color: Style.tileBackgroundColor +// radius: Style.cornerRadius + + +// Repeater { +// model: root.temperatureSchedules +// delegate: Rectangle { +// readonly property TemperatureSchedule temperatureSchedule: root.temperatureSchedules.get(index) +// readonly property int startMinutes: temperatureSchedule.startTime.getHours() * 60 + temperatureSchedule.startTime.getMinutes() +// readonly property int endMinutes: temperatureSchedule.endTime.getHours() * 60 + temperatureSchedule.endTime.getMinutes() +// readonly property int totalMinutes: 24 * 60 +// width: scheduleEditor.width +// // h : 24 = x : s +// y: startMinutes * slider.height / totalMinutes +// height: endMinutes * slider.height / totalMinutes - y +// radius: Style.cornerRadius +// color: Style.tileBackgroundColor +// Component.onCompleted: print("**created, startTime", startMinutes, endMinutes, totalMinutes, y, height) +// } +// } + +// Repeater { +// model: 24 +// delegate: Item { +// width: parent.width +// height: slider.height / 24 +// y: height * index +// Rectangle { +// width: parent.width +// height: 1 +// color: Style.gray +// visible: index > 0 +// } +// Label { +// width: parent.width +// text: { +// var d = new Date(); +// d.setHours(index,0,0); +// return d.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) +// } +// height: parent.height +// horizontalAlignment: Text.AlignHCenter +// verticalAlignment: Text.AlignVCenter +// font: Style.smallFont +// } +// } +// } + +// } +// MouseArea { +// anchors.fill: parent + +// property bool moveStart: false +// property bool moveEnd: false +// property TemperatureSchedule movedSchedule: null + +// property int startMouseY: 0 +// property int startMins: 0 +// property int endMins: 0 + +// onPressed: { +// startMouseY = mouseY +// var totalMins = 24 * 60 +// for (var i = 0; i < root.temperatureSchedules.count; i++) { +// var schedule = root.temperatureSchedules.get(i) +// print("schedule:", schedule.startTime, schedule.endTime) +// var startMin = schedule.startTime.getHours() * 60 + schedule.startTime.getMinutes() +// var startPos = startMin * slider.height / totalMins +// var endMin = schedule.endTime.getHours() * 60 + schedule.endTime.getMinutes() +// var endPos = endMin * slider.height / totalMins + +// startMins = startMin +// endMins = endMin +// movedSchedule = schedule +// if (Math.abs(startPos - mouseY) < 10) { +// moveStart = true; +// print("start") +// break; +// } else if (Math.abs(endPos - mouseY) < 10) { +// moveEnd = true +// print("end") +// break; +// } else if (mouseY > startPos && mouseY < endPos) { +// moveStart = true +// moveEnd = true +// print("middle") +// break; +// } +// } +// } +// onReleased: { +// moveStart = false; +// moveEnd = false; +// movedSchedule = null +// } + +// onPositionChanged: { +// var totalMins = 24 * 60 +// var diffY = mouseY - startMouseY +// // dY : height = dM : total +// var diffMins = diffY * totalMins / slider.height +// print("diffY", diffY, "diffMins", diffMins, startMins, startMins + diffMins) + +// var newStart = new Date(movedSchedule.startTime); +// var newEnd = new Date(movedSchedule.endTime); + +// var newStartMins = startMins + (moveStart ? diffMins : 0); +// var newEndMins = endMins + (moveEnd ? diffMins : 0); + + +// if (moveStart && !moveEnd) { +// newStartMins = Math.max(0, newStartMins) +// newStartMins = Math.min(newEndMins - 60, newStartMins) +// } else if (moveEnd && !moveStart) { +// newEndMins = Math.min(totalMins - 1, newEndMins) +// newEndMins = Math.max(newStartMins + 60, newEndMins) +// } else if (moveStart && moveEnd) { +// newStartMins = Math.max(0, newStartMins) +// newEndMins = Math.min(totalMins - 1, newEndMins) + +// var blockSize = endMins - startMins + +// newStartMins = Math.max(0, Math.min(newEndMins - blockSize, newStartMins)) +// newEndMins = Math.max(newStartMins + blockSize, newEndMins) + +// } + + + +// newStart.setHours(0, newStartMins) +// newEnd.setHours(0, newEndMins) + +// movedSchedule.startTime = newStart +// movedSchedule.endTime = newEnd + +// print("start time is new", newStart.toLocaleTimeString()) +// print("end time is new", newEnd.toLocaleTimeString()) + +// } +// } + +// ProgressButton { +// anchors.centerIn: parent +// imageSource: "add" +// onClicked: { +// var startTime = new Date() +// startTime.setHours(7, 0, 0) +// var endTime = new Date() +// endTime.setHours(22, 0 , 0) +// root.temperatureSchedules.createTemperatureSchedule(startTime, endTime, 21) +// } +// } +// } + + +} diff --git a/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml b/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml new file mode 100644 index 00000000..f3613a07 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml @@ -0,0 +1,44 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import QtCharts 2.3 + +NymeaToolTip { + width: layout.implicitWidth + Style.smallMargins * 2 + height: layout.implicitHeight + Style.smallMargins * 2 + + property Thing thing: null + property LogEntry entry: null + property alias color: rect.color + property ValueAxis axis: null + property int unit: Types.UnitNone + + readonly property int realY: entry ? Math.min(Math.max(mouseArea.height - (entry.value * mouseArea.height / axis.max) - height / 2 /*- Style.margins*/, 0), mouseArea.height - height) : 0 + property int fixedY: 0 + y: fixedY // Animated + + RowLayout { + id: layout + anchors.fill: parent + anchors.margins: Style.smallMargins + + Rectangle { + id: rect + width: Style.extraSmallFont.pixelSize + height: width + } + Label { + text: "%1: %2%3".arg(thing.name).arg(entry ? round(Types.toUiValue(entry.value, unit)) : "-").arg(Types.toUiUnit(unit)) + Layout.fillWidth: true + font: Style.extraSmallFont + elide: Text.ElideMiddle + function round(value) { + return Math.round(value * 100) / 100 + } + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/ZoneInfoField.qml b/nymea-app/ui/mainviews/airconditioning/ZoneInfoField.qml new file mode 100644 index 00000000..81b3d54a --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ZoneInfoField.qml @@ -0,0 +1,31 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import "qrc:/ui/delegates" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 + +RowLayout { + id: root + implicitHeight: Style.bigIconSize + + property alias imageSource: icon.name + property alias iconColor: icon.color + property alias text: label.text + + property ZoneInfo zone: null + property int flag: ZoneInfo.ZoneStatusFlagNone + property bool active: zone && ((zone.zoneStatus & flag) > 0) + + ColorIcon { + id: icon + size: Style.bigIconSize + color: root.active ? root.iconColor : Style.iconColor + } + Label { + id: label + Layout.fillWidth: true + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/ZoneInfoWrapper.qml b/nymea-app/ui/mainviews/airconditioning/ZoneInfoWrapper.qml new file mode 100644 index 00000000..0865f300 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ZoneInfoWrapper.qml @@ -0,0 +1,206 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU 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 General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtCharts 2.2 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" +import "qrc:/ui/delegates" + +Item { + id: root + + property ZoneInfo zone: null + + readonly property double zoneTemperature: d.zoneTemperature + readonly property double zoneHumidity: d.zoneHumidity + readonly property double zoneVOC: d.zoneVOC + + readonly property ThingsProxy thermostats: ThingsProxy { + engine: zone.thermostats.length > 0 ? _engine : null + shownThingIds: zone.thermostats + } + readonly property ThingsProxy heatingThermostats: ThingsProxy { + engine: _engine + parentProxy: thermostats + stateFilter: { "heatingOn": true } + } + readonly property ThingsProxy coolingThermostats: ThingsProxy { + engine: _engine + parentProxy: thermostats + stateFilter: { "coolingOn": true } + } + + readonly property ThingsProxy windowSensors: ThingsProxy { + engine: zone.windowSensors.length > 0 ? _engine : null + shownThingIds: zone.windowSensors + } + readonly property ThingsProxy openWindows: ThingsProxy { + engine: _engine + parentProxy: windowSensors + stateFilter: { "closed": false } + } + + readonly property ThingsProxy indoorSensors: ThingsProxy { + engine: root.zone.indoorSensors.length > 0 ? _engine : null + shownThingIds: root.zone.indoorSensors + } + readonly property ThingsProxy indoorTempSensors: ThingsProxy { + id: tempSensors + engine: _engine + parentProxy: indoorSensors + shownInterfaces: ["temperaturesensor"] + } + readonly property ThingsProxy indoorHumiditySensors: ThingsProxy { + engine: _engine + parentProxy: indoorSensors + shownInterfaces: ["humiditysensor"] + } + readonly property ThingsProxy indoorVocSensors: ThingsProxy { + engine: _engine + parentProxy: indoorSensors + shownInterfaces: ["vocsensor"] + } + readonly property ThingsProxy indoorPm25Sensors: ThingsProxy { + engine: _engine + parentProxy: indoorSensors + shownInterfaces: ["pm25sensor"] + } + + readonly property ThingsProxy outdoorSensors: ThingsProxy { + engine: root.zone.outdoorSensors.length > 0 ? _engine : null + shownThingIds: root.zone.outdoorSensors + } + readonly property ThingsProxy outdoorTempSensors: ThingsProxy { + engine: _engine + parentProxy: outdoorSensors + shownInterfaces: ["temperaturesensor"] + } + readonly property ThingsProxy outoorHumiditySensors: ThingsProxy { + engine: _engine + parentProxy: outdoorSensors + shownInterfaces: ["humiditysensor"] + } + readonly property ThingsProxy outdoorPm25Sensors: ThingsProxy { + engine: _engine + parentProxy: outdoorSensors + shownInterfaces: ["pm25sensor"] + } + + QtObject { + id: d + property double zoneTemperature + function updateZoneTemperature() { + var value = undefined; + if (thermostats.count > 0) { + for (var i = 0; i < thermostats.count; i++) { + var tempState = thermostats.get(i).stateByName("temperature") + if (!tempState) { + continue; + } + + if (value == undefined || tempState.value > value) { + value = tempState.value + } + } + } + if (value == undefined) { + for (var i = 0; i < indoorTempSensors.count; i++) { + var t = indoorTempSensors.get(i).stateByName("temperature").value + if (value == undefined || t > value) { + value = t + } + } + } + if (value != undefined) { + zoneTemperature = value; + } + } + + property double zoneHumidity + function updateZoneHumidity() { + var value = undefined; + for (var i = 0; i < indoorHumiditySensors.count; i++) { + var t = indoorHumiditySensors.get(i).stateByName("humidity").value + if (value == undefined || t > value) { + value = t; + } + } + if (value != undefined) { + zoneHumidity = value; + } + } + + property double zoneVOC + function updateZoneVOC() { + var value = undefined; + for (var i = 0; i < indoorVocSensors.count; i++) { + var t = indoorVocSensors.get(i).stateByName("voc").value + if (value == undefined || t > value) { + value = t; + } + } + if (value != undefined) { + zoneVOC = value; + } + } + } + + Repeater { + id: thingsRepeater + model: ThingsProxy { + engine: zone.thermostats.length > 0 || zone.indoorSensors.length > 0 ? _engine : null + shownThingIds: zone.thermostats + zone.indoorSensors + } + + delegate: Item { + readonly property Thing thing: indoorTempSensors.get(index) + readonly property State temperatureState: thing ? thing.stateByName("temperature") : null + property double temp: temperatureState ? temperatureState.value : 0 + onTempChanged: d.updateZoneTemperature() + readonly property State humidityState: thing ? thing.stateByName("humidity") : null + property double humidity: humidityState ? humidityState.value : 0 + onHumidityChanged: d.updateZoneHumidity() + readonly property State vocState: thing ? thing.stateByName("voc") : null + property double voc: vocState ? vocState.value : 0 + onVocChanged: d.updateZoneVOC() + } + onCountChanged: { + d.updateZoneTemperature() + d.updateZoneHumidity() + d.updateZoneVOC() + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/ZonePage.qml b/nymea-app/ui/mainviews/airconditioning/ZonePage.qml new file mode 100644 index 00000000..45bd7872 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ZonePage.qml @@ -0,0 +1,38 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 + +Page { + id: root + property AirConditioningManager acManager: null + property ZoneInfo zone: null + + ZoneInfoWrapper { + id: zoneWrapper + zone: root.zone + } + + + header: NymeaHeader { + text: root.zone.name + + onBackPressed: { + pageStack.pop() + } + + HeaderButton { + imageSource: "chart" + onClicked: pageStack.push(Qt.resolvedUrl("ACChartsPage.qml"), {acManager: root.acManager, zoneWrapper: zoneWrapper}) + } + } + + ZoneView { + anchors.fill: parent + acManager: root.acManager + zoneWrapper: zoneWrapper + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/ZoneStatusIcons.qml b/nymea-app/ui/mainviews/airconditioning/ZoneStatusIcons.qml new file mode 100644 index 00000000..4b7b2f76 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ZoneStatusIcons.qml @@ -0,0 +1,67 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.2 +import "qrc:/ui/components" +import Nymea 1.0 +import Nymea.AirConditioning 1.0 + +RowLayout { + id: root + Layout.fillWidth: true + + property ZoneInfo zone: null + property int iconSize: Style.iconSize + + signal clicked(int flag) + + Repeater { + id: zoneStatusRepeater + model: zoneStatusModel + property var zoneStatusModel: [ + { + value: ZoneInfo.ZoneStatusFlagSetpointOverrideActive, + icon: "dial", + activeColor: Style.accentColor + }, + { + value: ZoneInfo.ZoneStatusFlagTimeScheduleActive, + icon: "calendar", + activeColor: Style.orange + }, + { + value: ZoneInfo.ZoneStatusFlagWindowOpen, + icon: "sensors/window-closed", + activeIcon: "sensors/window-open", + activeColor: Style.red + }, + { + value: ZoneInfo.ZoneStatusFlagHighHumidity, + icon: "sensors/humidity", + activeColor: Style.lightBlue + }, + { + value: ZoneInfo.ZoneStatusFlagBadAir, + icon: "weathericons/weather-clouds", + activeColor: Style.purple + } + ] + delegate: Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.bigIconSize + property var entry: zoneStatusRepeater.zoneStatusModel[index] + ColorIcon { + id: zoneStatusIcon + anchors.centerIn: parent + name: entry.hasOwnProperty("activeIcon") && active ? entry.activeIcon : entry.icon + size: root.iconSize + property bool active: (root.zone.zoneStatus & entry.value) > 0 + color: active ? entry.activeColor : Style.iconColor + } + MouseArea { + anchors.fill: parent + onClicked: { + root.clicked(entry.value) + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/airconditioning/ZoneView.qml b/nymea-app/ui/mainviews/airconditioning/ZoneView.qml new file mode 100644 index 00000000..d680196a --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ZoneView.qml @@ -0,0 +1,182 @@ +import QtQuick 2.3 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.1 +import "qrc:/ui/components" +import "qrc:/ui/customviews" +import "qrc:/ui/delegates" +import Nymea 1.0 +import NymeaApp.Utils 1.0 +import Nymea.AirConditioning 1.0 + +Item { + id: root + property AirConditioningManager acManager: null + property ZoneInfoWrapper zoneWrapper: null + readonly property ZoneInfo zone: zoneWrapper.zone + + Flickable { + id: flickable + anchors.fill: parent + contentHeight: contentLayout.childrenRect.height + Style.margins + clip: true + + GridLayout { + id: contentLayout + width: parent.width + columns: app.landscape ? 2 : 1 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: flickable.height / 2 + implicitWidth: thermostat.implicitWidth + visible: zoneWrapper.thermostats.count > 0 + + CircleBackground { + id: thermostat + anchors { fill: parent; leftMargin: Style.hugeMargins; rightMargin: Style.hugeMargins; topMargin: Style.margins; bottomMargin: Style.margins } + + Dial { + id: thermostatDial + anchors.fill: parent + minValue: 10 + maxValue: 30 + precision: 0.5 + value: root.zone.currentSetpoint + color: pendingValue < activeValue ? Style.blue : Style.red + activeValue: zoneWrapper.zoneTemperature + + onMoved: { + acManager.setZoneSetpointOverride(root.zone.id, value, ZoneInfo.SetpointOverrideModeEventual, 0) + } + onClicked: { + var comp = Qt.createComponent(Qt.resolvedUrl("TimeOverrideDialog.qml")) + var dialog = comp.createObject(root, {acManager: root.acManager, zone: root.zone}) + dialog.open() + } + } + + ColumnLayout { + anchors.centerIn: parent + anchors.verticalCenterOffset: -Style.smallMargins + width: parent.contentItem.width * 0.6 + + Label { + Layout.fillWidth: true + text: Types.toUiUnit(Types.UnitDegreeCelsius) + font.pixelSize: Math.min(Style.smallFont.pixelSize, thermostat.contentItem.height / 16) + horizontalAlignment: Text.AlignHCenter + } + + Label { + Layout.fillWidth: true + text: Types.toUiValue(zone.currentSetpoint, Types.UnitDegreeCelsius).toFixed(1) + font.pixelSize: Math.min(Style.hugeFont.pixelSize, thermostat.contentItem.height / 8) + horizontalAlignment: Text.AlignHCenter + color: zoneWrapper.zoneTemperature == undefined + ? Stype.foregroundColor + : zone.currentSetpoint > zoneWrapper.zoneTemperature + ? Style.red + : zone.currentSetpoint < zoneWrapper.zoneTemperature + ? Style.blue + : Style.foregroundColor + } + Label { + Layout.fillWidth: true + text: Types.toUiValue(zoneWrapper.zoneTemperature, Types.UnitDegreeCelsius).toFixed(1) + font.pixelSize: Math.min(Style.largeFont.pixelSize, thermostat.contentItem.height / 12) + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + ColorIcon { + Layout.alignment: Qt.AlignHCenter + size: Style.smallIconSize + name: { + switch (root.zone.setpointOverrideMode) { + case ZoneInfo.SetpointOverrideModeUnlimited: + return "infinity" + case ZoneInfo.SetpointOverrideModeTimed: + return "alarm-clock" + case ZoneInfo.SetpointOverrideModeEventual: + return "event" + } + return "" + } + } + } + + ColorIcon { + anchors.horizontalCenter: thermostatDial.horizontalCenter + y: parent.contentItem.y + parent.contentItem.height - height - Style.smallMargins + size: Math.min(Style.bigIconSize, thermostatDial.height / 5) + name: zoneWrapper.heatingThermostats.count > 0 + ? "../images/thermostat/heating.svg" + : zoneWrapper.coolingThermostats.count > 0 + ? "../images/thermostat/cooling.svg" + : "" + color: zoneWrapper.heatingThermostats.count > 0 + ? app.interfaceToColor("heating") + : zoneWrapper.coolingThermostats.count > 0 + ? app.interfaceToColor("cooling") + : Style.iconColor + } + } + } + + + Item { + Layout.fillWidth: true + Layout.preferredHeight: zoneWrapper.thermostats.count > 0 ? flickable.height / 2 : flickable.height + implicitWidth: 800 + implicitHeight: statusIcons.implicitHeight + Layout.minimumHeight: statusIcons.implicitHeight + + BigZoneStatusIcons { + id: statusIcons + acManager: root.acManager + zoneWrapper: root.zoneWrapper + iconSize: Style.bigIconSize + width: parent.width - Style.margins * 2 + anchors.centerIn: parent + } + + ColorIcon { + anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: Style.margins } + name: "down" + opacity: zoneWrapper.indoorSensors.count > 0 && flickable.contentY - flickable.originY <= 0 && !app.landscape ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Style.animationDuration } } + } + } + + GridLayout { + Layout.fillWidth: true + columns: Math.ceil(width / 600) + Layout.columnSpan: contentLayout.columns + rowSpacing: 0 + columnSpacing: 0 + + Repeater { + model: zoneWrapper.indoorSensors + delegate: SensorListDelegate { + Layout.fillWidth: true + thing: zoneWrapper.indoorSensors.get(index) + } + } + } + } + } + + EmptyViewPlaceholder { + visible: zoneWrapper.thermostats.count == 0 && zoneWrapper.windowSensors.count == 0 && zoneWrapper.indoorSensors.count == 0 + anchors.centerIn: parent + width: parent.width - app.margins * 2 + title: qsTr("No things in this zone.") + text: qsTr("In order for this zone zo be useful, assign some things to it.") + imageSource: "/ui/images/sensors.svg" + buttonText: qsTr("Add things") + onButtonClicked: { + pageStack.push(Qt.resolvedUrl("EditZoneThingsPage.qml"), {acManager: acManager, zone: zone}) + } + } + +} diff --git a/nymea-app/ui/mainviews/airconditioning/ZonesView.qml b/nymea-app/ui/mainviews/airconditioning/ZonesView.qml new file mode 100644 index 00000000..81af1183 --- /dev/null +++ b/nymea-app/ui/mainviews/airconditioning/ZonesView.qml @@ -0,0 +1,127 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, 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 General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU 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 General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtCharts 2.2 +import Nymea 1.0 +import Nymea.AirConditioning 1.0 +import "qrc:/ui/components" +import "qrc:/ui/delegates" + +Flickable { + id: root + contentHeight: contentGrid.implicitHeight + + property AirConditioningManager acManager: null + + GridLayout { + id: contentGrid + width: parent.width - app.margins + anchors.horizontalCenter: parent.horizontalCenter + columns: Math.ceil(width / 600) + rowSpacing: 0 + columnSpacing: 0 + + Repeater { + model: !engine.thingManager.fetchingData ? acManager.zoneInfos : null + + + delegate: BigTile { + id: zoneDelegate + Layout.preferredWidth: contentGrid.width / contentGrid.columns + + readonly property ZoneInfo zone: acManager.zoneInfos.getZoneInfo(model.id) + ZoneInfoWrapper { + id: zoneWrapper + zone: zoneDelegate.zone + } + + header: RowLayout { + id: headerRow + width: parent.width + Layout.margins: Style.margins / 2 + Label { + Layout.fillWidth: true + text: zoneDelegate.zone.name + elide: Text.ElideRight + } +// ThingStatusIcons { +// thing: zoneDelegate.thermostat +// } + } + + contentItem: RowLayout { + spacing: Style.margins + + ColumnLayout { + + RowLayout { + ColorIcon { + name: app.interfaceToIcon("thermostat") + size: Style.smallIconSize + color: app.interfaceToColor("thermostat") + } + Label { + text: Types.toUiValue(zoneDelegate.zone.currentSetpoint, Types.UnitDegreeCelsius).toFixed(1) + Types.toUiUnit(Types.UnitDegreeCelsius) + font: Style.bigFont + } + } + + RowLayout { + ColorIcon { + name: app.interfaceToIcon("temperaturesensor") + size: Style.smallIconSize + color: app.interfaceToColor("temperaturesensor") + } + Label { + text: Types.toUiValue(zoneWrapper.zoneTemperature, Types.UnitDegreeCelsius).toFixed(1) + Types.toUiUnit(Types.UnitDegreeCelsius) + } + } + + } + + ZoneStatusIcons { + zone: zoneDelegate.zone + onClicked: zoneDelegate.clicked() + + } + } + + onClicked: { + pageStack.push(Qt.resolvedUrl("ZonePage.qml"), {zone: acManager.zoneInfos.get(index), acManager: acManager}) + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/dashboard/Dashboard.qml b/nymea-app/ui/mainviews/dashboard/Dashboard.qml index fdd92bfa..63bea909 100644 --- a/nymea-app/ui/mainviews/dashboard/Dashboard.qml +++ b/nymea-app/ui/mainviews/dashboard/Dashboard.qml @@ -462,7 +462,7 @@ MainViewBase { Component { id: editDialogComponent - MeaDialog { + NymeaDialog { id: editDialog standardButtons: Dialog.NoButton diff --git a/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml b/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml index b5b950b8..678d432e 100644 --- a/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml +++ b/nymea-app/ui/mainviews/dashboard/DashboardAddWizard.qml @@ -38,7 +38,7 @@ import NymeaApp.Utils 1.0 import "../../components" import "../../delegates" -MeaDialog { +NymeaDialog { id: root title: qsTr("Add item") diff --git a/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml index 880108f0..0cdff5ef 100644 --- a/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml +++ b/nymea-app/ui/mainviews/dashboard/DashboardFolderDelegate.qml @@ -72,7 +72,7 @@ DashboardDelegateBase { Component { id: configDialogComponent - MeaDialog { + NymeaDialog { id: configDialog onAccepted: { diff --git a/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml b/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml index d15b9115..da173fe1 100644 --- a/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml +++ b/nymea-app/ui/mainviews/dashboard/DashboardWebViewDelegate.qml @@ -154,7 +154,7 @@ DashboardDelegateBase { Component { id: configDialogComponent - MeaDialog { + NymeaDialog { id: configDialog onAccepted: { diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml index 875000cd..6a8a881b 100644 --- a/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml +++ b/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml @@ -37,6 +37,7 @@ Item { readonly property var startTime: { var date = new Date(fixTime(now)); date.setTime(date.getTime() - range * 60000 + 2000); + print("startTIme:", date) return date; } @@ -393,11 +394,11 @@ Item { return toStorageSeries.calculateValue(entry) + Math.max(0, -entry.acquisition) } function addEntry(entry) { - print("Adding return entry:", calculateValue(entry)) +// print("Adding return entry:", calculateValue(entry)) returnUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) } function insertEntry(index, entry) { - print("Adding return entry:", entry.acquisition, Math.max(0, -entry.acquisition), toStorageSeries.calculateValue(entry), calculateValue(entry)) +// print("Adding return entry:", entry.acquisition, Math.max(0, -entry.acquisition), toStorageSeries.calculateValue(entry), calculateValue(entry)) returnUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry)) } diff --git a/nymea-app/ui/system/GeneralSettingsPage.qml b/nymea-app/ui/system/GeneralSettingsPage.qml index 7f805d94..c7f5bd0f 100644 --- a/nymea-app/ui/system/GeneralSettingsPage.qml +++ b/nymea-app/ui/system/GeneralSettingsPage.qml @@ -162,7 +162,7 @@ SettingsPageBase { Layout.fillWidth: true } Label { - text: engine.systemController.serverTime.toLocaleTimeString(Locale.ShortTimeString) + text: engine.systemController.serverTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) Layout.fillWidth: true horizontalAlignment: Text.AlignRight } @@ -226,7 +226,7 @@ SettingsPageBase { text: qsTr("Restart %1").arg(Configuration.systemName) visible: engine.systemController.powerManagementAvailable && engine.jsonRpcClient.ensureServerVersion("5.1") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to restart %1 now?").arg(Configuration.systemName) var popup = dialog.createObject(app, { @@ -249,7 +249,7 @@ SettingsPageBase { text: qsTr("Reboot %1 system").arg(Configuration.systemName) visible: engine.systemController.powerManagementAvailable onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to reboot your %1 sytem now?").arg(Configuration.systemName) var popup = dialog.createObject(app, { @@ -271,7 +271,7 @@ SettingsPageBase { text: qsTr("Shut down %1 system").arg(Configuration.systemName) visible: engine.systemController.powerManagementAvailable onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to shut down your %1 sytem now?").arg(Configuration.systemName) var popup = dialog.createObject(app, { diff --git a/nymea-app/ui/system/ModbusRtuSettingsPage.qml b/nymea-app/ui/system/ModbusRtuSettingsPage.qml index f95b6c46..0a3b7c00 100644 --- a/nymea-app/ui/system/ModbusRtuSettingsPage.qml +++ b/nymea-app/ui/system/ModbusRtuSettingsPage.qml @@ -246,7 +246,7 @@ SettingsPageBase { Component { id: removeModbusMasterDialogComponent - MeaDialog { + NymeaDialog { id: removeModbusMasterDialog property ModbusRtuMaster modbusRtuMaster diff --git a/nymea-app/ui/system/NetworkSettingsPage.qml b/nymea-app/ui/system/NetworkSettingsPage.qml index e2f0e652..59fe8d7b 100644 --- a/nymea-app/ui/system/NetworkSettingsPage.qml +++ b/nymea-app/ui/system/NetworkSettingsPage.qml @@ -222,7 +222,7 @@ SettingsPageBase { checked: networkManager.networkingEnabled onClicked: { if (!checked) { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("Disabling networking will disconnect all connected clients. Be aware that you will not be able to interact remotely with this %1 system any more. Do not proceed unless you know what your are doing.").arg(Configuration.systemName) + "\n\n" + qsTr("Do you want to proceed?") @@ -309,7 +309,7 @@ SettingsPageBase { visible: networkManager.available && networkManager.networkingEnabled onClicked: { if (!checked) { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("Disabling WiFi will disconnect all clients connected via WiFi. Be aware that you will not be able to interact remotely with this %1 system any more unless a LAN cable is connected.").arg(Configuration.systemName) + "\n\n" + qsTr("Do you want to proceed?") diff --git a/nymea-app/ui/system/PackageDetailsPage.qml b/nymea-app/ui/system/PackageDetailsPage.qml index 26009323..a3559890 100644 --- a/nymea-app/ui/system/PackageDetailsPage.qml +++ b/nymea-app/ui/system/PackageDetailsPage.qml @@ -89,7 +89,7 @@ SettingsPageBase { visible: packageDetailsPage.pkg.updateAvailable || packageDetailsPage.pkg.installedVersion.length === 0 text: packageDetailsPage.pkg.updateAvailable ? qsTr("Update") : qsTr("Install") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 might not be functioning properly or restart during this time.").arg(Configuration.systemName) + "\n\n" + qsTr("\nDo you want to proceed?") @@ -113,7 +113,7 @@ SettingsPageBase { text: qsTr("Remove") visible: packageDetailsPage.pkg.canRemove onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 system might not be functioning properly during this time and restart during the process.\nDo you want to proceed?").arg(Configuration.systemName) var popup = dialog.createObject(app, { diff --git a/nymea-app/ui/system/SystemUpdatePage.qml b/nymea-app/ui/system/SystemUpdatePage.qml index 413e2341..5dfa7d9a 100644 --- a/nymea-app/ui/system/SystemUpdatePage.qml +++ b/nymea-app/ui/system/SystemUpdatePage.qml @@ -157,7 +157,7 @@ Page { visible: updatesModel.count > 0 enabled: !engine.systemController.updateManagementBusy onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("This will start a system update. Note that the update might take several minutes and your %1 system might not be functioning properly during this time and restart during the process.\nDo you want to proceed?").arg(Configuration.systemName) var popup = dialog.createObject(app, { @@ -231,7 +231,7 @@ Page { checked: model.enabled onClicked: { if (checked) { - var dialog = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("../components/NymeaDialog.qml")); var text = qsTr("Enabling additional software sources allows to install unreleased %1 packages.\nThis can potentially break your system and lead to problems.\nPlease only use this if you are sure you want this and consider reporting the issues you find when testing unreleased channels.").arg(Configuration.systemName) var popup = dialog.createObject(app, { diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index 9de50fb6..89f20db3 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -219,7 +219,7 @@ SettingsPageBase { Component { id: confirmTokenDeletionComponent - MeaDialog { + NymeaDialog { headerIcon: "../images/lock-closed.svg" title: qsTr("Remove device access") text: qsTr("Are you sure you want to remove %1 from accessing your %2 system?").arg("" + tokenInfo.deviceName + "").arg(Configuration.systemName) @@ -306,7 +306,7 @@ SettingsPageBase { Component { id: confirmUserDeletionComponent - MeaDialog { + NymeaDialog { headerIcon: "../images/lock-closed.svg" title: qsTr("Remove user") text: qsTr("Are you sure you want to remove %1 from accessing your %2 system?").arg("" + userInfo.username + "").arg(Configuration.systemName) diff --git a/nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml b/nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml index bdadf4f5..733f0035 100644 --- a/nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeNetworkPage.qml @@ -365,7 +365,7 @@ SettingsPageBase { Component { id: nodeInfoComponent - MeaDialog { + NymeaDialog { id: nodeInfoDialog property ZigbeeNode node: null property ThingsProxy nodeThings: null @@ -502,7 +502,7 @@ SettingsPageBase { Component { id: removeZigbeeNodeDialogComponent - MeaDialog { + NymeaDialog { id: removeZigbeeNodeDialog property ZigbeeNode zigbeeNode @@ -527,7 +527,7 @@ SettingsPageBase { Component { id: zigbeeHelpDialog - MeaDialog { + NymeaDialog { id: dialog title: qsTr("ZigBee network help") diff --git a/nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml b/nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml index 66aefe68..df953522 100644 --- a/nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeNetworkSettingsPage.qml @@ -163,7 +163,7 @@ SettingsPageBase { Layout.rightMargin: app.margins text: qsTr("Remove network") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to remove the network and all associated devices from the system?") var popup = dialog.createObject(app, { @@ -187,7 +187,7 @@ SettingsPageBase { Layout.rightMargin: app.margins text: qsTr("Factory reset controller") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to factory reset the controller? This will recreate the network and remove all associated devices from the system.") var popup = dialog.createObject(app, { diff --git a/nymea-app/ui/system/zigbee/ZigbeeNetworkTopologyPage.qml b/nymea-app/ui/system/zigbee/ZigbeeNetworkTopologyPage.qml index 8762c13f..350f5e6e 100644 --- a/nymea-app/ui/system/zigbee/ZigbeeNetworkTopologyPage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeNetworkTopologyPage.qml @@ -846,7 +846,7 @@ Page { Component { id: zigbeeHelpDialog - MeaDialog { + NymeaDialog { id: dialog title: qsTr("ZigBee topology help") diff --git a/nymea-app/ui/system/zigbee/ZigbeeNodePage.qml b/nymea-app/ui/system/zigbee/ZigbeeNodePage.qml index c0bbe316..c8b51479 100644 --- a/nymea-app/ui/system/zigbee/ZigbeeNodePage.qml +++ b/nymea-app/ui/system/zigbee/ZigbeeNodePage.qml @@ -314,7 +314,7 @@ SettingsPageBase { Component { id: removeZigbeeNodeDialogComponent - MeaDialog { + NymeaDialog { id: removeZigbeeNodeDialog property ZigbeeNode zigbeeNode @@ -409,7 +409,7 @@ SettingsPageBase { Component { id: addBindingComponent - MeaDialog { + NymeaDialog { title: qsTr("Add binding") Label { @@ -649,7 +649,7 @@ SettingsPageBase { Component { id: wakeupDialogComponent - MeaDialog { + NymeaDialog { id: wakeupDialog title: qsTr("Wake up %1").arg(root.node.model) text: qsTr("The selected node is a sleepy device. Please wake up the device by pressing a button.") diff --git a/nymea-app/ui/system/zwave/ZWaveNetworkPage.qml b/nymea-app/ui/system/zwave/ZWaveNetworkPage.qml index c4dc6dad..78fc80d6 100644 --- a/nymea-app/ui/system/zwave/ZWaveNetworkPage.qml +++ b/nymea-app/ui/system/zwave/ZWaveNetworkPage.qml @@ -346,7 +346,7 @@ SettingsPageBase { Component { id: nodeInfoComponent - MeaDialog { + NymeaDialog { id: nodeInfoDialog property ZWaveNode node: null property ThingsProxy nodeThings: null @@ -541,7 +541,7 @@ SettingsPageBase { Component { id: removeZWaveNodeDialogComponent - MeaDialog { + NymeaDialog { id: removeZWaveNodeDialog property ZWaveNode zwaveNode @@ -567,7 +567,7 @@ SettingsPageBase { Component { id: zigbeeHelpDialog - MeaDialog { + NymeaDialog { id: dialog title: qsTr("Z-Wave network help") @@ -614,7 +614,7 @@ SettingsPageBase { Component { id: addRemoveNodeDialogComponent - MeaDialog { + NymeaDialog { standardButtons: Dialog.Cancel onRejected: { zwaveManager.cancelPendingOperation(network.networkUuid) diff --git a/nymea-app/ui/system/zwave/ZWaveNetworkSettingsPage.qml b/nymea-app/ui/system/zwave/ZWaveNetworkSettingsPage.qml index 2f67bcd1..e293117e 100644 --- a/nymea-app/ui/system/zwave/ZWaveNetworkSettingsPage.qml +++ b/nymea-app/ui/system/zwave/ZWaveNetworkSettingsPage.qml @@ -165,7 +165,7 @@ SettingsPageBase { Layout.rightMargin: app.margins text: qsTr("Remove network") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to remove the network and all associated devices from the system?") var popup = dialog.createObject(app, { @@ -187,7 +187,7 @@ SettingsPageBase { Layout.rightMargin: app.margins text: qsTr("Factory reset controller") onClicked: { - var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/MeaDialog.qml")); + var dialog = Qt.createComponent(Qt.resolvedUrl("/ui/components/NymeaDialog.qml")); var text = qsTr("Are you sure you want to factory reset the controller? This will recreate the network and remove all associated devices from the system.") var popup = dialog.createObject(app, { diff --git a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml index e2d12fef..e1c476de 100644 --- a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml +++ b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml @@ -317,7 +317,7 @@ SettingsPageBase { Component { id: ioConnectionsDialogComponent - MeaDialog { + NymeaDialog { id: ioConnectionDialog standardButtons: Dialog.NoButton title: qsTr("Connect Inputs/Outputs") diff --git a/nymea-app/ui/thingconfiguration/NewThingPage.qml b/nymea-app/ui/thingconfiguration/NewThingPage.qml index 09119d1a..79315926 100644 --- a/nymea-app/ui/thingconfiguration/NewThingPage.qml +++ b/nymea-app/ui/thingconfiguration/NewThingPage.qml @@ -31,7 +31,6 @@ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 -import QtQuick.Controls.Material 2.1 import Nymea 1.0 import "../components" import "../delegates" @@ -39,6 +38,8 @@ import "../delegates" Page { id: root + property string filterInterface: "" + header: NymeaHeader { text: qsTr("Set up new thing") onBackPressed: { @@ -55,158 +56,146 @@ Page { page.aborted.connect(function() { pageStack.pop(); }) - } - Pane { - id: filterPane - anchors { left: parent.left; top: parent.top; right: parent.right } - Behavior on height { NumberAnimation { duration: 120; easing.type: Easing.InOutQuad } } + ColumnLayout { + anchors.fill: parent - height: implicitHeight + app.margins * 2 - Material.elevation: 1 - z: 1 + GridLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + columnSpacing: app.margins + columns: Math.max(1, Math.floor(width / 250)) * 2 + visible: root.filterInterface == "" + z: 1 + Label { + text: qsTr("Vendor") + } - leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0 - contentItem: Item { - clip: true - GridLayout { - anchors.fill: parent - anchors.margins: app.margins - columnSpacing: app.margins - columns: Math.max(1, Math.floor(width / 250)) * 2 - Label { - text: qsTr("Vendor") + ComboBox { + id: vendorFilterComboBox + Layout.fillWidth: true + textRole: "displayName" + currentIndex: -1 + VendorsProxy { + id: vendorsProxy + vendors: engine.thingManager.vendors } + model: ListModel { + id: vendorsFilterModel + dynamicRoles: true - ComboBox { - id: vendorFilterComboBox - Layout.fillWidth: true - textRole: "displayName" - currentIndex: -1 - VendorsProxy { - id: vendorsProxy - vendors: engine.thingManager.vendors + Component.onCompleted: { + append({displayName: qsTr("All"), vendorId: ""}) + for (var i = 0; i < vendorsProxy.count; i++) { + var vendor = vendorsProxy.get(i); + append({displayName: vendor.displayName, vendorId: vendor.id}) + } + vendorFilterComboBox.currentIndex = 0 } - model: ListModel { - id: vendorsFilterModel - dynamicRoles: true + } + } + Label { + text: qsTr("Type") + } - Component.onCompleted: { - append({displayName: qsTr("All"), vendorId: ""}) - for (var i = 0; i < vendorsProxy.count; i++) { - var vendor = vendorsProxy.get(i); - append({displayName: vendor.displayName, vendorId: vendor.id}) - } - vendorFilterComboBox.currentIndex = 0 + ComboBox { + id: typeFilterComboBox + Layout.fillWidth: true + textRole: "displayName" + InterfacesSortModel { + id: interfacesSortModel + interfacesModel: InterfacesModel { + engine: _engine + shownInterfaces: app.supportedInterfaces + showUncategorized: false + } + } + model: ListModel { + id: typeFilterModel + ListElement { interfaceName: ""; displayName: qsTr("All") } + + Component.onCompleted: { + for (var i = 0; i < interfacesSortModel.count; i++) { + append({interfaceName: interfacesSortModel.get(i), displayName: app.interfaceToString(interfacesSortModel.get(i))}); } } } - Label { - text: qsTr("Type") + } + + Item { + Layout.preferredHeight: Style.iconSize + Layout.minimumWidth: Style.iconSize + + ColorIcon { + size: Style.iconSize + name: "../images/find.svg" } + } - ComboBox { - id: typeFilterComboBox - Layout.fillWidth: true - textRole: "displayName" - InterfacesSortModel { - id: interfacesSortModel - interfacesModel: InterfacesModel { - engine: _engine - shownInterfaces: app.supportedInterfaces - showUncategorized: false - } - } - model: ListModel { - id: typeFilterModel - ListElement { interfaceName: ""; displayName: qsTr("All") } + TextField { + id: displayNameFilterField + Layout.fillWidth: true + } + } - Component.onCompleted: { - for (var i = 0; i < interfacesSortModel.count; i++) { - append({interfaceName: interfacesSortModel.get(i), displayName: app.interfaceToString(interfacesSortModel.get(i))}); - } - } - } + GroupedListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + bottomMargin: packagesFilterModel.count > 0 ? height : 0 + + model: ThingClassesProxy { + id: thingClassesProxy + engine: _engine + filterInterface: root.filterInterface != "" ? root.filterInterface : typeFilterModel.get(typeFilterComboBox.currentIndex).interfaceName + includeProvidedInterfaces: true + filterVendorId: vendorFilterComboBox.currentIndex >= 0 ? vendorsFilterModel.get(vendorFilterComboBox.currentIndex).vendorId : "" + filterString: displayNameFilterField.displayText + groupByInterface: true + } + + onContentYChanged: print("contentY", contentY, contentHeight, originY) + + delegate: NymeaItemDelegate { + id: tingClassDelegate + width: parent.width + text: model.displayName + subText: engine.thingManager.vendors.getVendor(model.vendorId).displayName + iconName: app.interfacesToIcon(thingClass.interfaces) + prominentSubText: false + wrapTexts: false + + property ThingClass thingClass: thingClassesProxy.get(index) + + onClicked: { + root.startWizard(thingClass) } + } - Item { - Layout.preferredHeight: Style.iconSize - Layout.minimumWidth: Style.iconSize - - ColorIcon { - height: parent.height - width: height - name: "../images/find.svg" - } + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - Style.margins * 2 + opacity: packagesFilterModel.count > 0 && + (thingClassesProxy.count == 0 || listView.contentY >= listView.contentHeight + listView.originY) + ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Style.shortAnimationDuration } } + visible: opacity > 0 + title: qsTr("Looking for something else?") + text: qsTr("Try to install more plugins.") + imageSource: "/ui/images/save.svg" + buttonText: qsTr("Install plugins") + onButtonClicked: { + pageStack.push(Qt.resolvedUrl("/ui/system/PackageListPage.qml"), {filter: "nymea-plugin-"}) } - - TextField { - id: displayNameFilterField - Layout.fillWidth: true + PackagesFilterModel { + id: packagesFilterModel + packages: engine.systemController.packages + nameFilter: "nymea-plugin-" } } } } - GroupedListView { - id: listView - anchors { - left: parent.left - top: filterPane.bottom - right: parent.right - bottom: parent.bottom - } - bottomMargin: packagesFilterModel.count > 0 ? height : 0 - - model: ThingClassesProxy { - id: thingClassesProxy - engine: _engine - filterInterface: typeFilterModel.get(typeFilterComboBox.currentIndex).interfaceName - includeProvidedInterfaces: true - filterVendorId: vendorFilterComboBox.currentIndex >= 0 ? vendorsFilterModel.get(vendorFilterComboBox.currentIndex).vendorId : "" - filterString: displayNameFilterField.displayText - groupByInterface: true - } - - onContentYChanged: print("contentY", contentY, contentHeight, originY) - - delegate: NymeaItemDelegate { - id: tingClassDelegate - width: parent.width - text: model.displayName - subText: engine.thingManager.vendors.getVendor(model.vendorId).displayName - iconName: app.interfacesToIcon(thingClass.interfaces) - prominentSubText: false - wrapTexts: false - - property ThingClass thingClass: thingClassesProxy.get(index) - - onClicked: { - root.startWizard(thingClass) - } - } - - EmptyViewPlaceholder { - anchors.centerIn: parent - width: parent.width - Style.margins * 2 - opacity: packagesFilterModel.count > 0 && - (thingClassesProxy.count == 0 || listView.contentY >= listView.contentHeight + listView.originY) - ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: Style.shortAnimationDuration } } - visible: opacity > 0 - title: qsTr("Looking for something else?") - text: qsTr("Try to install more plugins.") - imageSource: "/ui/images/save.svg" - buttonText: qsTr("Install plugins") - onButtonClicked: { - pageStack.push(Qt.resolvedUrl("/ui/system/PackageListPage.qml"), {filter: "nymea-plugin-"}) - } - PackagesFilterModel { - id: packagesFilterModel - packages: engine.systemController.packages - nameFilter: "nymea-plugin-" - } - } - } } diff --git a/nymea-app/ui/utils/AirQualityIndex.qml b/nymea-app/ui/utils/AirQualityIndex.qml index 1691a882..6f979753 100644 --- a/nymea-app/ui/utils/AirQualityIndex.qml +++ b/nymea-app/ui/utils/AirQualityIndex.qml @@ -5,6 +5,17 @@ import Nymea 1.0 Item { id: root + function currentIndex(scale, value) { + for (var i = 0; i < scale.length; i++) { + if (value <= scale[i].value) { + return scale[i]; + } + } + log.warn("Value out of scale!") + return null + } + + property var caqiPm10: [ { "value": 25,