Add air conditioning experience

This commit is contained in:
Michael Zanetti 2022-10-22 23:04:56 +02:00
parent 8a8149951b
commit 79841a5e27
114 changed files with 7770 additions and 931 deletions

View File

@ -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<PlatformHelper>("Nymea", 1, 0, "PlatformHelper", platformHelperProvider);
qmlRegisterSingletonType(QUrl("qrc:///ui/utils/NymeaUtils.qml"), "Nymea", 1, 0, "NymeaUtils" );
qmlRegisterType<NfcThingActionWriter>("Nymea", 1, 0, "NfcThingActionWriter");

View File

@ -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}
}

View File

@ -0,0 +1,23 @@
#include "airconditioning_plugin.h"
#include "airconditioningmanager.h"
#include "airconditioningmanager.h"
#include "zoneinfo.h"
#include <qqml.h>
#include <QDebug>
void AirconditioningPlugin::registerTypes(const char *uri)
{
qCritical() << "################# loading plugin";
// @uri Nymea.AirConditioning
qmlRegisterType<AirConditioningManager>(uri, 1, 0, "AirConditioningManager");
qmlRegisterUncreatableType<ZoneInfos>(uri, 1, 0, "ZoneInfos", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<ZoneInfo>(uri, 1, 0, "ZoneInfo", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<TemperatureSchedule>(uri, 1, 0, "TemperatureSchedule", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<TemperatureDaySchedule>(uri, 1, 0, "TemperatureDaySchedule", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<TemperatureWeekSchedule>(uri, 1, 0, "TemperatureWeekSchedule", "Get it from AirConditioningManager");
}

View File

@ -0,0 +1,15 @@
#ifndef AIRCONDITIONING_PLUGIN_H
#define AIRCONDITIONING_PLUGIN_H
#include <QQmlExtensionPlugin>
class AirconditioningPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri) override;
};
#endif // AIRCONDITIONING_PLUGIN_H

View File

@ -0,0 +1,389 @@
#include "airconditioningmanager.h"
#include "zoneinfo.h"
#include "engine.h"
#include <QJsonDocument>
#include <QMetaEnum>
#include "logging.h"
NYMEA_LOGGING_CATEGORY(dcAirConditioningExperience, "AirConditioningExperience")
AirConditioningManager::AirConditioningManager(QObject *parent)
: QObject{parent},
m_zoneInfos(new ZoneInfos(this))
{
qRegisterMetaType<ZoneInfo::SetpointOverrideMode>();
}
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<QUuid> &thermostats, const QList<QUuid> &windowSensors, const QList<QUuid> &indoorSensors, const QList<QUuid> &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<ZoneInfo::SetpointOverrideMode>();
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<QUuid> &thermostats, const QList<QUuid> &windowSensors, const QList<QUuid> &indoorSensors, const QList<QUuid> &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<QUuid> 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<QUuid> 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<QUuid> 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<QUuid> 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 &params)
{
Q_UNUSED(commandId)
qCDebug(dcAirConditioningExperience()) << "Add zone response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(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 &params)
{
qCDebug(dcAirConditioningExperience()) << "remove zone response" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(metaEnum.keyToValue(params.value("error").toByteArray().data()));
emit removeZoneReply(commandId, error);
}
void AirConditioningManager::getZonesResponse(int commandId, const QVariantMap &params)
{
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 &params)
{
qCDebug(dcAirConditioningExperience()) << "set zone name response" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(metaEnum.keyToValue(params.value("error").toByteArray().data()));
emit setZoneNameReply(commandId, error);
}
void AirConditioningManager::setZoneStandbySetpointResponse(int commandId, const QVariantMap &params)
{
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(metaEnum.keyToValue(params.value("error").toByteArray().data()));
emit setZoneStandbySetpointReply(commandId, error);
}
void AirConditioningManager::setZoneSetpointOverrideResponse(int commandId, const QVariantMap &params)
{
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(metaEnum.keyToValue(params.value("error").toByteArray().data()));
emit setZoneSetpointOverrideReply(commandId, error);
}
void AirConditioningManager::setZoneWeekScheduleResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcAirConditioningExperience()) << "set zone week schedule response" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(metaEnum.keyToValue(params.value("error").toByteArray().data()));
emit setZoneWeekScheduleReply(commandId, error);
}
void AirConditioningManager::setZoneThingsResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcAirConditioningExperience()) << "set zone things response" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<AirConditioningError>();
AirConditioningError error = static_cast<AirConditioningError>(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>();
ZoneInfo::ZoneStatus zoneStatus = ZoneInfo::ZoneStatusFlagNone;
foreach (const QVariant &flag, zoneMap.value("zoneStatus").toList()) {
zoneStatus.setFlag(static_cast<ZoneInfo::ZoneStatusFlag>(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>();
ZoneInfo::SetpointOverrideMode mode = static_cast<ZoneInfo::SetpointOverrideMode>(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<QUuid> 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;
}

View File

@ -0,0 +1,81 @@
#ifndef AIRCONDITIONINGMANAGER_H
#define AIRCONDITIONINGMANAGER_H
#include <QObject>
#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<QUuid> &thermostats, const QList<QUuid> &windowSensors, const QList<QUuid> &indoorSensors, const QList<QUuid> &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<QUuid> &thermostats, const QList<QUuid> &windowSensors, const QList<QUuid> &indoorSensors, const QList<QUuid> &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 &params);
void removeZoneResponse(int commandId, const QVariantMap &params);
void getZonesResponse(int commandId, const QVariantMap &params);
void setZoneNameResponse(int commandId, const QVariantMap &params);
void setZoneStandbySetpointResponse(int commandId, const QVariantMap &params);
void setZoneSetpointOverrideResponse(int commandId, const QVariantMap &params);
void setZoneWeekScheduleResponse(int commandId, const QVariantMap &params);
void setZoneThingsResponse(int commandId, const QVariantMap &params);
private:
ZoneInfo *unpack(const QVariantMap &zoneMap, ZoneInfo *zone = nullptr);
private:
Engine *m_engine = nullptr;
ZoneInfos *m_zoneInfos = nullptr;
};
#endif // AIRCONDITIONINGMANAGER_H

View File

@ -0,0 +1,29 @@
#ifndef LIBNYMEAAPPAIRCONDITIONING_H
#define LIBNYMEAAPPAIRCONDITIONING_H
#include "airconditioningmanager.h"
#include "zoneinfo.h"
#include <qqml.h>
namespace Nymea
{
namespace AirConditioning
{
void registerQmlTypes() {
const char uri[] = "Nymea.AirConditioning";
// @uri Nymea.AirConditioning
qmlRegisterType<AirConditioningManager>(uri, 1, 0, "AirConditioningManager");
qmlRegisterUncreatableType<ZoneInfos>(uri, 1, 0, "ZoneInfos", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<ZoneInfo>(uri, 1, 0, "ZoneInfo", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<TemperatureSchedule>(uri, 1, 0, "TemperatureSchedule", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<TemperatureDaySchedule>(uri, 1, 0, "TemperatureDaySchedule", "Get it from AirConditioningManager");
qmlRegisterUncreatableType<TemperatureWeekSchedule>(uri, 1, 0, "TemperatureWeekSchedule", "Get it from AirConditioningManager");
}
}
}
#endif // LIBNYMEAAPPAIRCONDITIONING_H

View File

@ -0,0 +1,2 @@
module Nymea.AirConditioning
plugin AirConditioning

View File

@ -0,0 +1,215 @@
#include "temperatureschedule.h"
#include <QDebug>
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<int, QByteArray> 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;
}

View File

@ -0,0 +1,92 @@
#ifndef TEMPERATURESCHEDULE_H
#define TEMPERATURESCHEDULE_H
#include <QObject>
#include <QTime>
#include <QAbstractListModel>
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<int, QByteArray> 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<TemperatureSchedule*> 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<int, QByteArray> roleNames() const override { return QHash<int, QByteArray>(); }
Q_INVOKABLE TemperatureDaySchedule* get(int index) const;
Q_INVOKABLE TemperatureWeekSchedule *clone() const; // Passes ownership to caller
private:
QList<TemperatureDaySchedule*> m_list;
};
#endif // TEMPERATURESCHEDULE_H

View File

@ -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<QUuid> ZoneInfo::thermostats() const
{
return m_thermostats;
}
void ZoneInfo::setThermostats(const QList<QUuid> &thermostats)
{
if (m_thermostats != thermostats) {
m_thermostats = thermostats;
emit thermostatsChanged();
}
}
QList<QUuid> ZoneInfo::windowSensors() const
{
return m_windowSensors;
}
void ZoneInfo::setWindowSensors(const QList<QUuid> &windowSensors)
{
if (m_windowSensors != windowSensors) {
m_windowSensors = windowSensors;
emit windowSensorsChanged();
}
}
QList<QUuid> ZoneInfo::indoorSensors() const
{
return m_indoorSensors;
}
void ZoneInfo::setIndoorSensors(const QList<QUuid> &indoorSensors)
{
if (m_indoorSensors != indoorSensors) {
m_indoorSensors = indoorSensors;
emit indoorSensorsChanged();
}
}
QList<QUuid> ZoneInfo::outdoorSensors() const
{
return m_outdoorSensors;
}
void ZoneInfo::setOutdoorSensors(const QList<QUuid> &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<int, QByteArray> 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;
}

View File

@ -0,0 +1,138 @@
#ifndef ZONEINFO_H
#define ZONEINFO_H
#include <QObject>
#include <QUuid>
#include <QAbstractListModel>
#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<QUuid> thermostats READ thermostats NOTIFY thermostatsChanged)
Q_PROPERTY(QList<QUuid> windowSensors READ windowSensors NOTIFY windowSensorsChanged)
Q_PROPERTY(QList<QUuid> indoorSensors READ indoorSensors NOTIFY indoorSensorsChanged)
Q_PROPERTY(QList<QUuid> 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<QUuid> thermostats() const;
void setThermostats(const QList<QUuid> &thermostats);
QList<QUuid> windowSensors() const;
void setWindowSensors(const QList<QUuid> &windowSensors);
QList<QUuid> indoorSensors() const;
void setIndoorSensors(const QList<QUuid> &indoorSensors);
QList<QUuid> outdoorSensors() const;
void setOutdoorSensors(const QList<QUuid> &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<QUuid> m_thermostats;
QList<QUuid> m_windowSensors;
QList<QUuid> m_indoorSensors;
QList<QUuid> 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<int, QByteArray> 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<ZoneInfo*> m_list;
};
#endif // ZONEINFO_H

View File

@ -0,0 +1,3 @@
TEMPLATE = subdirs
SUBDIRS += airconditioning

View File

@ -35,7 +35,6 @@
#include <QUuid>
#include <QUrl>
#include <QHostAddress>
#include <QBluetoothAddress>
#include <QObject>
#include <QAbstractListModel>
#include <QDateTime>

View File

@ -284,6 +284,7 @@ void EnergyLogs::getLogsResponse(int commandId, const QVariantMap &params)
double minValue = 0, maxValue = 0;
qCDebug(dcEnergyLogs()) << "Logs response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
QList<EnergyLogEntry*> 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");
}

View File

@ -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 <QtQml/qqml.h>
namespace Nymea
{
namespace Core
{
static QObject* interfacesModel_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
{
Q_UNUSED(engine)
@ -270,6 +276,7 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<LogEntry>(uri, 1, 0, "LogEntry", "Get them from LogsModel");
qmlRegisterType<BarSeriesAdapter>(uri, 1, 0, "BarSeriesAdapter");
qmlRegisterType<XYSeriesAdapter>(uri, 1, 0, "XYSeriesAdapter");
qmlRegisterType<BoolSeriesAdapter>(uri, 1, 0, "BoolSeriesAdapter");
qmlRegisterUncreatableType<TagsManager>(uri, 1, 0, "TagsManager", "Get it from Engine");
qmlRegisterUncreatableType<Tags>(uri, 1, 0, "Tags", "Get it from TagsManager");
@ -382,5 +389,6 @@ void registerQmlTypes() {
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
}
}
}
#endif // LIBNYMEAAPPCORE_H

View File

@ -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 \

View File

@ -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;
}

View File

@ -0,0 +1,49 @@
#ifndef BOOLSERIESADAPTER_H
#define BOOLSERIESADAPTER_H
#include "logsmodel.h"
#include <QObject>
#include <QXYSeries>
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

View File

@ -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<LogEntry*> newBlock;
QList<QVariant> 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";
}

View File

@ -144,6 +144,7 @@ protected:
int m_generatedEntries = 0;
QDateTime m_fetchStartTime;
};
#endif // LOGSMODEL_H

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -75,7 +75,7 @@ private:
class Sample {
public:
QDateTime timestamp; // The timestamp where this sample *ends*
QList<LogEntry*> entries; // all log entries in this sample, that is, from timestamp - smaple size to timestamp
QVector<LogEntry*> 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;

View File

@ -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<int, QByteArray> Things::roleNames() const
roles[RoleSetupDisplayMessage] = "setupDisplayMessage";
roles[RoleInterfaces] = "interfaces";
roles[RoleBaseInterface] = "baseInterface";
roles[RoleMainInterface] = "mainInterface";
return roles;
}

View File

@ -52,7 +52,8 @@ public:
RoleSetupStatus,
RoleSetupDisplayMessage,
RoleInterfaces,
RoleBaseInterface
RoleBaseInterface,
RoleMainInterface
};
Q_ENUM(Roles)

View File

@ -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<QUuid> 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 &paramsFilter)
}
}
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);
}

View File

@ -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 &paramsFilter);
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<QUuid> m_shownThingClassIds;
QList<QUuid> m_hiddenThingClassIds;
QList<QUuid> m_shownThingIds;
QList<QUuid> 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;

View File

@ -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();
}

View File

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

View File

@ -299,5 +299,10 @@
<file>ui/images/plus.svg</file>
<file>ui/images/minus.svg</file>
<file>ui/images/sensors/vibration.svg</file>
<file>ui/images/calendar.svg</file>
<file>ui/images/sensors/window-closed.svg</file>
<file>ui/images/sensors/window-open.svg</file>
<file>ui/images/infinity.svg</file>
<file>ui/images/edit-paste.svg</file>
</qresource>
</RCC>

View File

@ -38,6 +38,7 @@
#include <QCommandLineOption>
#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);

View File

@ -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 = /

View File

@ -23,7 +23,7 @@
<file>ui/components/Graph.qml</file>
<file>ui/components/ErrorDialog.qml</file>
<file>ui/components/ShutterControls.qml</file>
<file>ui/components/MeaDialog.qml</file>
<file>ui/components/NymeaDialog.qml</file>
<file>ui/components/MainPageTabButton.qml</file>
<file>ui/components/AutoSizeMenu.qml</file>
<file>ui/components/EmptyViewPlaceholder.qml</file>
@ -289,5 +289,25 @@
<file>ui/mainviews/energy/ConsumerStatsPage.qml</file>
<file>ui/mainviews/energy/ConsumersPieChartPage.qml</file>
<file>ui/connection/ManualConnectionEntry.qml</file>
<file>ui/mainviews/AirConditioningView.qml</file>
<file>ui/mainviews/airconditioning/ZoneView.qml</file>
<file>ui/mainviews/airconditioning/TimeSchedulePage.qml</file>
<file>ui/mainviews/airconditioning/TemperatureScheduleEditor.qml</file>
<file>ui/customviews/SensorView.qml</file>
<file>ui/components/MultiSelectionTabs.qml</file>
<file>ui/mainviews/airconditioning/ZonePage.qml</file>
<file>ui/mainviews/airconditioning/ZonesView.qml</file>
<file>ui/mainviews/airconditioning/ACSettingsPage.qml</file>
<file>ui/components/StateDial.qml</file>
<file>ui/delegates/SensorListDelegate.qml</file>
<file>ui/mainviews/airconditioning/ZoneStatusIcons.qml</file>
<file>ui/mainviews/airconditioning/BigZoneStatusIcons.qml</file>
<file>ui/mainviews/airconditioning/ZoneInfoWrapper.qml</file>
<file>ui/mainviews/airconditioning/ACChartsPage.qml</file>
<file>ui/mainviews/airconditioning/TimeOverrideDialog.qml</file>
<file>ui/mainviews/airconditioning/TooltipDelegate.qml</file>
<file>ui/mainviews/airconditioning/EditZonePage.qml</file>
<file>ui/mainviews/airconditioning/EditZoneThingsPage.qml</file>
<file>ui/mainviews/airconditioning/LegendDelegate.qml</file>
</qresource>
</RCC>

View File

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

View File

@ -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":

View File

@ -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,

View File

@ -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)

View File

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

View File

@ -34,7 +34,7 @@ import QtQuick.Layouts 1.1
import Nymea 1.0
import "../delegates"
MeaDialog {
NymeaDialog {
id: root
property Thing thing

View File

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

View File

@ -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
}
}

View File

@ -162,7 +162,7 @@ Dialog {
Component {
id: addManualConnectionComponent
MeaDialog {
NymeaDialog {
id: addManualConnectionDialog
standardButtons: Dialog.Ok | Dialog.Cancel
property alias rpcUrl: manualEntry.rpcUrl

View File

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

View File

@ -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!")

View File

@ -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.")

View File

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

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
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;
}
}
}

View File

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

View File

@ -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)

View File

@ -35,7 +35,7 @@ import QtQuick.Layouts 1.3
import Nymea 1.0
import "../components"
MeaDialog {
NymeaDialog {
id: root
title: qsTr("Insecure connection")

View File

@ -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)
}
}
}
}

View File

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

View File

@ -76,6 +76,7 @@ MainPageTile {
switch (iface.name) {
case "heating":
case "cooling":
case "thermostat":
case "sensor":
page = "SensorsDeviceListPage.qml"
break;

View File

@ -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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
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
}
}
}
}
}

View File

@ -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
}
}
}
}
}
}
}

View File

@ -90,7 +90,7 @@ ThingPageBase {
}
}
Dial {
StateDial {
anchors.centerIn: parent
height: background.contentItem.height
width: background.contentItem.width

View File

@ -317,7 +317,7 @@ ThingPageBase {
Component {
id: detailsPopup
MeaDialog {
NymeaDialog {
id: detailsDialog
property string timestamp
property bool accessGranted

View File

@ -234,7 +234,7 @@ ThingPageBase {
Component {
id: detailsPopup
MeaDialog {
NymeaDialog {
id: detailsDialog
standardButtons: Dialog.NoButton
property string timestamp

View File

@ -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)
}
}
}
}

View File

@ -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))

View File

@ -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")
}
}

View File

@ -0,0 +1,213 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="calendar.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="13.720701"
inkscape:cx="25.570847"
inkscape:cy="41.433733"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065"
inkscape:locked="false" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067"
inkscape:locked="false" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069"
inkscape:locked="false" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071"
inkscape:locked="false" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077"
inkscape:locked="false" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076"
inkscape:locked="false" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078"
inkscape:locked="false" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080"
inkscape:locked="false" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170"
inkscape:locked="false" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 23.976562 8.0019531 C 18.943803 8.0601401 15.26124 7.881546 12.25 9.5429688 C 10.74438 10.37368 9.5531414 11.778707 8.8945312 13.533203 C 8.2359314 15.287699 7.9980469 17.369641 7.9980469 20 L 7.9980469 48.001953 L 7.9980469 76 C 7.9980469 78.630359 8.2359314 80.714254 8.8945312 82.46875 C 9.5531414 84.223246 10.74438 85.62632 12.25 86.457031 C 15.26124 88.118454 18.943803 87.941823 23.976562 88 L 23.988281 88 L 47.998047 88 L 72.009766 88 L 72.021484 88 C 77.054244 87.94182 80.736807 88.118454 83.748047 86.457031 C 85.253667 85.62632 86.444916 84.223246 87.103516 82.46875 C 87.762113 80.714254 88 78.630359 88 76 L 88 48.001953 L 83.998047 48.001953 L 83.998047 76 C 83.998047 78.369642 83.747742 80.022711 83.357422 81.0625 C 82.967112 82.102279 82.547973 82.550368 81.814453 82.955078 C 80.350163 83.762988 77.036307 83.941223 71.998047 84 L 71.974609 84 L 47.998047 84 L 24.021484 84 L 24 84 C 18.96042 83.941263 15.648104 83.763108 14.183594 82.955078 C 13.450074 82.550368 13.030935 82.102279 12.640625 81.0625 C 12.250305 80.022711 12 78.369642 12 76 L 12 48.001953 L 12 32 C 12 29.630358 12.250305 27.977279 12.640625 26.9375 C 13.030935 25.897711 13.450074 25.449632 14.183594 25.044922 C 15.650624 24.235492 18.967304 24.058477 24.021484 24 L 47.998047 24 L 71.998047 24 C 77.036307 24.05877 80.350163 24.237002 81.814453 25.044922 C 82.547973 25.449632 82.967112 25.897711 83.357422 26.9375 C 83.747742 27.977279 83.998047 29.630358 83.998047 32 L 83.998047 48 L 88 48 L 88 20 C 88 17.369641 87.762116 15.287699 87.103516 13.533203 C 86.444916 11.778707 85.253667 10.37368 83.748047 9.5429688 C 80.736807 7.881546 77.054244 8.0601301 72.021484 8.0019531 L 72.009766 8.0019531 L 72 8.0019531 L 72 16 L 66 16 L 66 8.0019531 L 30 8.0019531 L 30 16 L 24 16 L 24 8.0019531 L 23.988281 8.0019531 L 23.976562 8.0019531 z "
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
id="path4410" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10;marker:none;enable-background:accumulate"
d="M 30 0 L 24 1 L 24 8 L 30 8 L 30 0 z M 72 0 L 66 1 L 66 8 L 72 8 L 72 0 z "
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
id="path4430" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.0023737;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 387.98438,405.36133 v 12 h 12.00195 v -12 z"
id="path4277"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.0023737;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 387.98438,369.36133 v 12 h 12.00195 v -12 z"
id="path4279"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.0023737;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 387.98438,387.36133 v 12 h 12.00195 v -12 z"
id="path4281"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.0023737;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 369.97656,405.36133 v 12 h 12.00196 v -12 z"
id="path4283"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.0023737;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 369.97656,369.36133 v 12 h 12.00196 v -12 z"
id="path4285"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.0023737;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 369.97656,387.36133 v 12 h 12.00196 v -12 z"
id="path4287"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="edit-paste.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6199994"
inkscape:cx="-13.932387"
inkscape:cy="64.741981"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 27.976562 12 C 22.943803 12.058187 19.26124 11.883489 16.25 13.544922 C 14.74438 14.375633 13.553141 15.778707 12.894531 17.533203 C 12.235931 19.287699 11.998047 21.369641 11.998047 24 L 11.998047 72.001953 C 11.998047 74.632312 12.235931 76.712301 12.894531 78.466797 C 13.553141 80.221293 14.74438 81.628273 16.25 82.458984 C 18.839866 83.887918 21.985059 83.954121 26 83.982422 L 26 79.957031 C 22.120071 79.86816 19.436048 79.646109 18.183594 78.955078 C 17.450074 78.550358 17.030935 78.102289 16.640625 77.0625 C 16.250305 76.022721 16 74.371596 16 72.001953 L 16 24 C 16 21.630358 16.250305 19.977279 16.640625 18.9375 C 17.030935 17.897711 17.450074 17.449632 18.183594 17.044922 C 19.650624 16.235502 22.967304 16.058377 28.021484 16 L 44 16 C 41.76772 16 40 14.233606 40 12 L 27.988281 12 L 27.976562 12 z M 44 16 L 59.998047 16 C 65.036307 16.058767 68.350163 16.237012 69.814453 17.044922 C 70.547973 17.449632 70.967112 17.897711 71.357422 18.9375 C 71.747742 19.977279 71.998047 21.630358 71.998047 24 L 71.998047 34 L 76 34 L 76 24 C 76 21.369641 75.762116 19.287699 75.103516 17.533203 C 74.444916 15.778707 73.253667 14.375633 71.748047 13.544922 C 68.736807 11.883489 65.054244 12.058177 60.021484 12 L 60.009766 12 L 47.998047 12 C 47.998047 14.233606 46.23228 16 44 16 z M 32 80 L 32 84 L 34 84 L 34 80 L 32 80 z "
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
id="path4412" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 345.9668,357.36133 0,52 60.02343,0 0,-2 0,-50 -60.02343,0 z m 4,4.00195 52.02148,0 0,43.99805 -52.02148,0 0,-43.99805 z"
id="rect4154"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path4214"
d="m 369.97549,397.36222 -4.00158,0 0,-20 4.00158,0 z"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
d="m 393.985,397.36222 -4.00158,0 0,-28.0481 4.00158,0 z"
id="path4212"
inkscape:connector-curvature="0" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
d="m 381.98025,397.36222 -4.00158,0 0,-28.0481 4.00158,0 z"
id="path4210"
inkscape:connector-curvature="0" />
<path
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="M 32 12 L 32 24 L 56 24 L 56 12 L 47.998047 12 C 47.998047 14.233606 46.23228 16 44 16 C 41.76772 16 40 14.233606 40 12 L 32 12 z "
transform="matrix(0,-1,-1.0003957,0,438.00245,441.36222)"
id="rect4235" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 434,397.36133 c 0,4.39491 -3.60657,8 -8.00195,8 -4.39538,0 -8.00196,-3.60509 -8.00196,-8 0,-4.39492 3.60658,-7.99805 8.00196,-7.99805 4.39538,0 8.00195,3.60313 8.00195,7.99805 z m -4,0 c 0,-2.23228 -1.76746,-3.99805 -4.00195,-3.99805 -2.23449,0 -4.00196,1.76577 -4.00196,3.99805 0,2.23228 1.76747,4 4.00196,4 2.23449,0 4.00195,-1.76772 4.00195,-4 z"
id="path4237"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
viewBox="0 0 96 96.000001"
sodipodi:docname="infinity.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="6.376014"
inkscape:cx="51.99173"
inkscape:cy="54.893229"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="false"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:pagecheckerboard="0"
inkscape:window-width="1466"
inkscape:window-height="933"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#808080;fill-opacity:1;stroke:none"
d="m 364.0904,368.96573 c -0.0215,-0.0161 -0.0354,-0.0404 -0.0567,-0.0566 -0.0253,-0.0201 -0.057,-0.0305 -0.0821,-0.0508 z"
id="path4157" />
<path
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#808080;fill-opacity:1;stroke:none"
d="m 364.07673,417.80167 -0.13873,0.10742 c 0.0251,-0.0203 0.0569,-0.0307 0.0821,-0.0508 0.0214,-0.0162 0.0353,-0.0405 0.0567,-0.0566 z"
id="path4344" />
<path
style="fill:#808080;stroke-width:0.999998;fill-opacity:1"
d="m 371.95478,418.30264 c 0.24926,3.13425 0.93495,5.76101 2.17203,8.32048 2.71658,5.62048 7.60074,9.5375 13.08642,10.49507 3.09535,0.54034 6.30444,0.1776 9.12588,-1.03153 1.90511,-0.81642 3.57858,-1.94492 5.1622,-3.48105 0.84546,-0.82012 1.40319,-1.45284 2.08938,-2.37024 2.20576,-2.94909 3.70213,-6.6451 4.21857,-10.41992 0.16497,-1.20584 0.20743,-1.84193 0.20807,-3.11731 0.003,-4.86859 -1.26247,-9.16486 -3.84663,-13.06521 -1.62907,-2.45882 -3.79545,-4.83006 -7.06388,-7.7319 -1.22553,-1.08809 -2.78609,-2.41926 -5.0592,-4.31559 -3.23605,-2.69968 -4.96327,-4.18444 -6.38164,-5.48586 -5.23998,-4.80783 -7.56775,-8.73184 -8.24091,-13.89193 -0.10484,-0.80325 -0.12016,-3.42467 -0.0247,-4.19462 0.49014,-3.94881 2.01911,-7.24057 4.52507,-9.74207 1.44156,-1.43899 2.9441,-2.38759 4.72496,-2.98307 1.1383,-0.38061 2.11473,-0.53885 3.32492,-0.53885 0.4414,0 0.99864,0.0259 1.23824,0.0573 3.50298,0.46163 6.59486,2.53651 8.78395,5.89464 1.33995,2.05555 2.17236,4.37112 2.52957,7.03688 0.10402,0.77665 0.11877,3.44756 0.0233,4.21755 -0.2163,1.74266 -0.60748,3.31581 -1.1992,4.8225 -1.36394,3.47306 -3.61088,6.30686 -8.22394,10.37189 -0.39788,0.35063 -0.71779,0.65237 -0.71086,0.67055 0.005,0.0181 0.47695,0.4206 1.04448,0.8943 1.00693,0.84046 2.28029,1.91591 2.80817,2.37178 l 0.26291,0.227 0.19572,-0.16082 c 1.08304,-0.88997 3.8177,-3.56682 4.82972,-4.72763 1.86172,-2.1354 3.06847,-3.91218 4.08298,-6.01158 2.00131,-4.14141 2.78966,-8.89606 2.21945,-13.3861 -0.4445,-3.50021 -1.65193,-6.76005 -3.54034,-9.55823 -3.0475,-4.51569 -7.50753,-7.38573 -12.43672,-8.00302 -1.24517,-0.15591 -2.89688,-0.13478 -4.20036,0.0536 -4.82084,0.69735 -9.25001,3.67222 -12.21075,8.20137 -1.9621,3.00145 -3.1565,6.57158 -3.48147,10.40634 -0.0321,0.3782 -0.0566,1.26525 -0.0543,1.97124 0.0161,5.0741 1.40517,9.54091 4.20542,13.52364 1.1094,1.57788 2.03567,2.66636 3.71914,4.37041 1.93605,1.95977 3.53183,3.37472 8.28045,7.34227 5.70313,4.76499 7.80641,6.69629 9.61071,8.8249 2.77343,3.27197 4.23575,6.40038 4.74633,10.15421 0.13742,1.01049 0.17299,3.05173 0.0702,4.02251 -0.53219,5.01995 -2.99255,9.44138 -6.59262,11.84745 -1.9309,1.2905 -4.4554,1.96292 -6.68794,1.78137 -1.53195,-0.12456 -2.66833,-0.43993 -4.06799,-1.12884 -2.5496,-1.25492 -4.71691,-3.50238 -6.16332,-6.39123 -2.63176,-5.25627 -2.28598,-11.99321 0.88528,-17.24804 0.64901,-1.07539 1.46719,-2.17354 2.48503,-3.3354 0.96103,-1.097 3.03704,-3.112 4.6482,-4.51163 0.25855,-0.2246 0.47009,-0.42416 0.47009,-0.44346 0,-0.0545 -4.01139,-3.42983 -4.12405,-3.47024 -0.0853,-0.0307 -2.37694,2.093 -3.55767,3.29678 -2.86128,2.91713 -4.71223,5.55335 -5.99019,8.53157 -0.99109,2.30968 -1.59042,4.64899 -1.85367,7.23506 -0.0858,0.84332 -0.12342,3.0609 -0.0645,3.80242 z"
id="path2480" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
id="svg4874"
height="96"
viewBox="0 0 96 96.000001"
width="96"
version="1.1"
sodipodi:docname="window-closed.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="true"
inkscape:zoom="2.2194323"
inkscape:cx="11.489425"
inkscape:cy="68.485981"
inkscape:window-width="1466"
inkscape:window-height="933"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4874">
<inkscape:grid
type="xygrid"
id="grid861" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(67.857 -78.505)">
<rect
id="rect4782"
style="color:#000000;fill:none"
transform="rotate(90)"
height="96"
width="96"
y="-28.143"
x="78.505" />
<path
id="path4643"
style="color-rendering:auto;text-decoration-color:#000000;color:#000000;font-variant-numeric:normal;shape-rendering:auto;solid-color:#000000;text-decoration-line:none;fill:#808080;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;image-rendering:auto;white-space:normal;text-decoration-style:solid;font-variant-ligatures:none;isolation:auto;text-transform:none"
d="m-43.869 86.504-0.01172 0.002c-5.0328 0.05818-8.7136-0.12027-11.725 1.541-1.5055 0.83064-2.6968 2.2356-3.3555 3.9902-0.65866 1.7547-0.89648 3.8364-0.89648 6.4668v56.002c0 2.6304 0.23782 4.7121 0.89648 6.4668 0.65866 1.7546 1.85 3.1596 3.3555 3.9902 3.011 1.6613 6.6918 1.4848 11.725 1.543h0.01172 48.023 0.011719c5.0328-0.0582 8.7136 0.11832 11.725-1.543 1.5055-0.83064 2.6968-2.2356 3.3555-3.9902 0.65866-1.7547 0.89648-3.8364 0.89648-6.4668v-56.002c0-2.6304-0.23782-4.7121-0.89648-6.4668-0.66-1.759-1.851-3.163-3.356-3.994-3.011-1.661-6.6922-1.483-11.725-1.541l-0.011719-0.002h-48.023zm0.01172 4h48c5.0383 0.05877 8.3519 0.23688 9.8164 1.0449 0.73364 0.40478 1.1527 0.85491 1.543 1.8945 0.39025 1.0396 0.64062 2.691 0.64062 5.0605v56.002c0 2.3696-0.25037 4.0209-0.64062 5.0606-0.39025 1.0396-0.80933 1.4898-1.543 1.8945-1.4645 0.80804-4.7782 0.98616-9.8164 1.0449h-47.977-0.02344c-5.0383-0.0588-8.3519-0.23688-9.8164-1.0449-0.73364-0.40478-1.1508-0.85491-1.541-1.8945-0.39025-1.0396-0.64258-2.691-0.64258-5.0606v-56.002c0-2.3696 0.25232-4.0209 0.64258-5.0605 0.39025-1.0396 0.80738-1.4898 1.541-1.8945 1.4645-0.80804 4.7782-0.98616 9.8164-1.0449z" />
</g>
<path
style="fill:none;stroke:#808080;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 21,46 46.866844,20.621709"
id="path859" />
<path
style="fill:none;stroke:#808080;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 21,59.999999 59.999999,21.736601"
id="path1215" />
<path
style="fill:none;stroke:#808080;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 35,60 60.866844,34.621709"
id="path1217" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
id="svg4874"
height="96"
viewBox="0 0 96 96.000001"
width="96"
version="1.1"
sodipodi:docname="window-open.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs9" />
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="true"
inkscape:zoom="2.2194323"
inkscape:cx="11.489425"
inkscape:cy="68.485981"
inkscape:window-width="1466"
inkscape:window-height="933"
inkscape:window-x="70"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4874">
<inkscape:grid
type="xygrid"
id="grid861" />
</sodipodi:namedview>
<metadata
id="metadata4879">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(67.857 -78.505)">
<rect
id="rect4782"
style="color:#000000;fill:none"
transform="rotate(90)"
height="96"
width="96"
y="-28.143"
x="78.505" />
<path
id="path4643"
style="color-rendering:auto;text-decoration-color:#000000;color:#000000;font-variant-numeric:normal;shape-rendering:auto;solid-color:#000000;text-decoration-line:none;fill:#808080;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;image-rendering:auto;white-space:normal;text-decoration-style:solid;font-variant-ligatures:none;isolation:auto;text-transform:none"
d="m-43.869 86.504-0.01172 0.002c-5.0328 0.05818-8.7136-0.12027-11.725 1.541-1.5055 0.83064-2.6968 2.2356-3.3555 3.9902-0.65866 1.7547-0.89648 3.8364-0.89648 6.4668v56.002c0 2.6304 0.23782 4.7121 0.89648 6.4668 0.65866 1.7546 1.85 3.1596 3.3555 3.9902 3.011 1.6613 6.6918 1.4848 11.725 1.543h0.01172 48.023 0.011719c5.0328-0.0582 8.7136 0.11832 11.725-1.543 1.5055-0.83064 2.6968-2.2356 3.3555-3.9902 0.65866-1.7547 0.89648-3.8364 0.89648-6.4668v-56.002c0-2.6304-0.23782-4.7121-0.89648-6.4668-0.66-1.759-1.851-3.163-3.356-3.994-3.011-1.661-6.6922-1.483-11.725-1.541l-0.011719-0.002h-48.023zm0.01172 4h48c5.0383 0.05877 8.3519 0.23688 9.8164 1.0449 0.73364 0.40478 1.1527 0.85491 1.543 1.8945 0.39025 1.0396 0.64062 2.691 0.64062 5.0605v56.002c0 2.3696-0.25037 4.0209-0.64062 5.0606-0.39025 1.0396-0.80933 1.4898-1.543 1.8945-1.4645 0.80804-4.7782 0.98616-9.8164 1.0449h-47.977-0.02344c-5.0383-0.0588-8.3519-0.23688-9.8164-1.0449-0.73364-0.40478-1.1508-0.85491-1.541-1.8945-0.39025-1.0396-0.64258-2.691-0.64258-5.0606v-56.002c0-2.3696 0.25232-4.0209 0.64258-5.0605 0.39025-1.0396 0.80738-1.4898 1.541-1.8945 1.4645-0.80804 4.7782-0.98616 9.8164-1.0449z" />
</g>
<g
id="g1449"
transform="matrix(-0.78565286,0,0,0.78565286,72.177621,16.288596)"
style="stroke-width:1.27283">
<path
id="path4237"
d="m 15.500006,49.750872 h 43.749997 v -3.5 H 15.500006 Z"
style="fill:#808080;stroke-width:1.27283"
inkscape:connector-curvature="0" />
<path
id="path5588-9-2-96-04"
d="m 52.254379,37.500085 0.007,20.999999 c 3.193837,-1.460113 6.445249,-3.093913 9.755374,-4.898863 3.279237,-1.809324 6.442449,-3.675437 9.487624,-5.600262 -3.045,-1.886237 -6.208212,-3.734499 -9.487624,-5.543824 -3.312225,-1.805913 -6.565387,-3.457913 -9.760624,-4.95705 z"
style="color:#000000;fill:#808080;stroke-width:1.27283"
inkscape:connector-curvature="0" />
</g>
<g
id="g1504"
transform="matrix(0.78565286,0,0,0.78565286,23.822379,4.288596)"
style="stroke-width:1.27283">
<path
id="path1500"
d="m 15.500006,49.750872 h 43.749997 v -3.5 H 15.500006 Z"
style="fill:#808080;stroke-width:1.27283"
inkscape:connector-curvature="0" />
<path
id="path1502"
d="m 52.254379,37.500085 0.007,20.999999 c 3.193837,-1.460113 6.445249,-3.093913 9.755374,-4.898863 3.279237,-1.809324 6.442449,-3.675437 9.487624,-5.600262 -3.045,-1.886237 -6.208212,-3.734499 -9.487624,-5.543824 -3.312225,-1.805913 -6.565387,-3.457913 -9.760624,-4.95705 z"
style="color:#000000;fill:#808080;stroke-width:1.27283"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -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."),

View File

@ -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?"),

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
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})
}
}
}
}
}

View File

@ -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")
}
}
}
}

View File

@ -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")
}
}
}
}

View File

@ -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})
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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;
}
}
}
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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})
}
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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)
// }
// }
// }
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
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()
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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})
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
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})
}
}
}
}
}

View File

@ -462,7 +462,7 @@ MainViewBase {
Component {
id: editDialogComponent
MeaDialog {
NymeaDialog {
id: editDialog
standardButtons: Dialog.NoButton

View File

@ -38,7 +38,7 @@ import NymeaApp.Utils 1.0
import "../../components"
import "../../delegates"
MeaDialog {
NymeaDialog {
id: root
title: qsTr("Add item")

View File

@ -72,7 +72,7 @@ DashboardDelegateBase {
Component {
id: configDialogComponent
MeaDialog {
NymeaDialog {
id: configDialog
onAccepted: {

View File

@ -154,7 +154,7 @@ DashboardDelegateBase {
Component {
id: configDialogComponent
MeaDialog {
NymeaDialog {
id: configDialog
onAccepted: {

View File

@ -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))
}

View File

@ -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,
{

Some files were not shown because too many files have changed in this diff Show More