diff --git a/airconditioningjsonhandler.cpp b/airconditioningjsonhandler.cpp index bd8572f..88d8031 100644 --- a/airconditioningjsonhandler.cpp +++ b/airconditioningjsonhandler.cpp @@ -63,6 +63,7 @@ AirConditioningJsonHandler::AirConditioningJsonHandler(AirConditioningManager *m params.insert("o:windowSensors", QVariantList() << enumValueName(Uuid)); params.insert("o:indoorSensors", QVariantList() << enumValueName(Uuid)); params.insert("o:outdoorSensors", QVariantList() << enumValueName(Uuid)); + params.insert("o:notifications", QVariantList() << enumValueName(Uuid)); returns.insert("airConditioningError", enumRef()); returns.insert("o:zone", objectRef()); registerMethod("AddZone", description, params, returns); @@ -106,10 +107,11 @@ AirConditioningJsonHandler::AirConditioningJsonHandler(AirConditioningManager *m params.clear(); returns.clear(); description = "Set Zone things"; params.insert("zoneId", enumValueName(Uuid)); - params.insert("thermostats", QVariantList() << enumValueName(Uuid)); - params.insert("windowSensors", QVariantList() << enumValueName(Uuid)); - params.insert("indoorSensors", QVariantList() << enumValueName(Uuid)); - params.insert("outdoorSensors", QVariantList() << enumValueName(Uuid)); + params.insert("o:thermostats", QVariantList() << enumValueName(Uuid)); + params.insert("o:windowSensors", QVariantList() << enumValueName(Uuid)); + params.insert("o:indoorSensors", QVariantList() << enumValueName(Uuid)); + params.insert("o:outdoorSensors", QVariantList() << enumValueName(Uuid)); + params.insert("o:notifications", QVariantList() << enumValueName(Uuid)); returns.insert("airConditioningError", enumRef()); registerMethod("SetZoneThings", description, params, returns); @@ -161,7 +163,7 @@ JsonReply *AirConditioningJsonHandler::GetZones(const QVariantMap ¶ms) JsonReply *AirConditioningJsonHandler::AddZone(const QVariantMap ¶ms) { - QList thermostats, windowSensors, indoorSensors, outdoorSensors; + QList thermostats, windowSensors, indoorSensors, outdoorSensors, notifications; foreach (const QVariant &id, params.value("thermostats").toList()) { thermostats.append(id.toUuid()); } @@ -174,7 +176,10 @@ JsonReply *AirConditioningJsonHandler::AddZone(const QVariantMap ¶ms) foreach (const QVariant &id, params.value("outdoorSensors").toList()) { outdoorSensors.append(id.toUuid()); } - QPair status = m_manager->addZone(params.value("name").toString(), thermostats, windowSensors, indoorSensors, outdoorSensors); + foreach (const QVariant &id, params.value("notificatiosn").toList()) { + notifications.append(id.toUuid()); + } + QPair status = m_manager->addZone(params.value("name").toString(), thermostats, windowSensors, indoorSensors, outdoorSensors, notifications); QVariantMap ret = { {"airConditioningError", enumValueName(status.first)} }; @@ -230,19 +235,50 @@ JsonReply *AirConditioningJsonHandler::SetZoneWeekSchedule(const QVariantMap &pa JsonReply *AirConditioningJsonHandler::SetZoneThings(const QVariantMap ¶ms) { QUuid zoneId = params.value("zoneId").toUuid(); - QList thermostats, windowSensors, indoorSensors, outdoorSensors; - foreach (const QVariant &variant, params.value("thermostats").toList()) { - thermostats.append(ThingId(variant.toUuid())); + + ZoneInfo zone = m_manager->zone(zoneId); + + QList thermostats, windowSensors, indoorSensors, outdoorSensors, notifications; + if (params.contains("thermostats")) { + foreach (const QVariant &variant, params.value("thermostats").toList()) { + thermostats.append(ThingId(variant.toUuid())); + } + } else { + thermostats = zone.thermostats(); } - foreach (const QVariant &variant, params.value("windowSensors").toList()) { - windowSensors.append(ThingId(variant.toUuid())); + + if (params.contains("windowSensors")) { + foreach (const QVariant &variant, params.value("windowSensors").toList()) { + windowSensors.append(ThingId(variant.toUuid())); + } + } else { + windowSensors = zone.windowSensors(); } - foreach (const QVariant &variant, params.value("indoorSensors").toList()) { - indoorSensors.append(ThingId(variant.toUuid())); + + if (params.contains("indoorSensors")) { + foreach (const QVariant &variant, params.value("indoorSensors").toList()) { + indoorSensors.append(ThingId(variant.toUuid())); + } + } else { + indoorSensors = zone.indoorSensors(); } - foreach (const QVariant &variant, params.value("outdoorSensors").toList()) { - outdoorSensors.append(ThingId(variant.toUuid())); + + if (params.contains("outdoorSensors")) { + foreach (const QVariant &variant, params.value("outdoorSensors").toList()) { + outdoorSensors.append(ThingId(variant.toUuid())); + } + } else { + outdoorSensors = zone.outdoorSensors(); } - AirConditioningManager::AirConditioningError status = m_manager->setZoneThings(zoneId, thermostats, windowSensors, indoorSensors, outdoorSensors); + + if (params.contains("notifications")) { + foreach (const QVariant &variant, params.value("notifications").toList()) { + notifications.append(ThingId(variant.toUuid())); + } + } else { + notifications = zone.notifications(); + } + + AirConditioningManager::AirConditioningError status = m_manager->setZoneThings(zoneId, thermostats, windowSensors, indoorSensors, outdoorSensors, notifications); return createReply({{"airConditioningError", enumValueName(status)}}); } diff --git a/airconditioningmanager.cpp b/airconditioningmanager.cpp index c3de964..760c555 100644 --- a/airconditioningmanager.cpp +++ b/airconditioningmanager.cpp @@ -52,6 +52,9 @@ AirConditioningManager::AirConditioningManager(ThingManager *thingManager, QObje if (thing->thingClass().interfaces().contains("thermostat")) { m_thermostats.insert(thing->id(), new Thermostat(m_thingManager, thing, this)); } + if (thing->thingClass().interfaces().contains("notifications")) { + m_notifications.insert(thing->id(), new Notifications(m_thingManager, thing, this)); + } } loadZones(); @@ -71,17 +74,17 @@ ZoneInfos AirConditioningManager::zones() const return m_zones.values(); } -ZoneInfo AirConditioningManager::zone(const ThingId &thermostatId) +ZoneInfo AirConditioningManager::zone(const QUuid &zoneId) { - return m_zones.value(thermostatId); + return m_zones.value(zoneId); } -QPair AirConditioningManager::addZone(const QString &name, const QList &thermostats, const QList windowSensors, const QList indoorSensors, const QList outdoorSensors) +QPair AirConditioningManager::addZone(const QString &name, const QList &thermostats, const QList windowSensors, const QList indoorSensors, const QList outdoorSensors, const QList notifications) { ZoneInfo zone(QUuid::createUuid()); zone.setName(name); zone.setWeekSchedule(TemperatureWeekSchedule::create()); - AirConditioningError status = verifyThingIds(thermostats, windowSensors, indoorSensors, outdoorSensors); + AirConditioningError status = verifyThingIds(thermostats, windowSensors, indoorSensors, outdoorSensors, notifications); if (status != AirConditioningErrorNoError) { qCWarning(dcAirConditioning()) << "Invalid thing id" << status << "in" << thermostats; return qMakePair(status, ZoneInfo()); @@ -91,6 +94,7 @@ QPair AirConditioningMan zone.setWindowSensors(windowSensors); zone.setIndoorSensors(indoorSensors); zone.setOutdoorSensors(outdoorSensors); + zone.setNotifications(notifications); m_zones.insert(zone.id(), zone); saveZones(); @@ -179,12 +183,12 @@ AirConditioningManager::AirConditioningError AirConditioningManager::setZoneWeek return AirConditioningErrorNoError; } -AirConditioningManager::AirConditioningError AirConditioningManager::setZoneThings(const QUuid &zoneId, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors) +AirConditioningManager::AirConditioningError AirConditioningManager::setZoneThings(const QUuid &zoneId, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors, const QList ¬ifications) { if (!m_zones.contains(zoneId)) { return AirConditioningErrorZoneNotFound; } - AirConditioningError status = verifyThingIds(thermostats, windowSensors, indoorSensors, outdoorSensors); + AirConditioningError status = verifyThingIds(thermostats, windowSensors, indoorSensors, outdoorSensors, notifications); if (status != AirConditioningErrorNoError) { return status; } @@ -192,8 +196,9 @@ AirConditioningManager::AirConditioningError AirConditioningManager::setZoneThin m_zones[zoneId].setWindowSensors(windowSensors); m_zones[zoneId].setIndoorSensors(indoorSensors); m_zones[zoneId].setOutdoorSensors(outdoorSensors); + m_zones[zoneId].setNotifications(notifications); saveZones(); - qCDebug(dcAirConditioning()) << "Zone things set. Thermostats:" << thermostats << "Window sensors:" << windowSensors << "indoor sensors:" << indoorSensors << "outdoor sensors:" << outdoorSensors; + qCDebug(dcAirConditioning()) << "Zone things set. Thermostats:" << thermostats << "Window sensors:" << windowSensors << "indoor sensors:" << indoorSensors << "outdoor sensors:" << outdoorSensors << "notifications:" << notifications; emit zoneChanged(m_zones.value(zoneId)); updateZone(zoneId); return AirConditioningErrorNoError; @@ -219,6 +224,10 @@ void AirConditioningManager::onThingAdded(Thing *thing) qCInfo(dcAirConditioning()) << "Thermostat added:" << thing; m_thermostats.insert(thing->id(), new Thermostat(m_thingManager, thing, this)); } + if (thing->thingClass().interfaces().contains("notifications")) { + qCInfo(dcAirConditioning()) << "Notifications added:" << thing; + m_notifications.insert(thing->id(), new Notifications(m_thingManager, thing, this)); + } } void AirConditioningManager::onThingRemoved(const ThingId &thingId) @@ -228,6 +237,7 @@ void AirConditioningManager::onThingRemoved(const ThingId &thingId) QList windowSensors = m_zones.value(zone.id()).windowSensors(); QList indoorSensors = m_zones.value(zone.id()).indoorSensors(); QList outdoorSensors = m_zones.value(zone.id()).outdoorSensors(); + QList notifications = m_zones.value(zone.id()).notifications(); bool changed = false; if (thermostats.contains(thingId)) { thermostats.removeAll(thingId); @@ -245,28 +255,37 @@ void AirConditioningManager::onThingRemoved(const ThingId &thingId) outdoorSensors.removeAll(thingId); changed = true; } + if (notifications.contains(thingId)) { + notifications.removeAll(thingId); + changed = true; + } if (changed) { - setZoneThings(zone.id(), thermostats, windowSensors, indoorSensors, outdoorSensors); + setZoneThings(zone.id(), thermostats, windowSensors, indoorSensors, outdoorSensors, notifications); } } } void AirConditioningManager::onThingStateChaged(Thing *thing, const StateTypeId &stateTypeId, const QVariant &value, const QVariant &minValue, const QVariant &maxValue) { - Q_UNUSED(stateTypeId) - Q_UNUSED(value) Q_UNUSED(minValue) Q_UNUSED(maxValue) - // We'll only want to immediately react on window open/close changes. Anything else is good enough once per minute - if (!thing->thingClass().interfaces().contains("closablesensor")) { - return; - } - - + StateType stateType = thing->thingClass().getStateType(stateTypeId); foreach (const ZoneInfo &zone, m_zones) { - if (zone.windowSensors().contains(thing->id())) { - qCDebug(dcAirConditioning()) << "Window sensor in zone" << zone.name() << "changed"; + bool changed = false; + if (zone.windowSensors().contains(thing->id()) && stateType.name() == "closed") { + qCDebug(dcAirConditioning()) << "Window sensor in zone" << zone.name() << "changed" << value; + changed = true; + } + if (zone.thermostats().contains(thing->id()) && stateType.name() == "temperature") { + qCDebug(dcAirConditioning()) << "Thermostat temperature sensor in zone" << zone.name() << "changed" << value; + changed = true; + } + if (zone.indoorSensors().contains(thing->id()) && QStringList{"temperature", "humidity", "voc", "pm25"}.contains(stateType.name())) { + qCDebug(dcAirConditioning()) << "Sensor for" << stateType.name() << "in zone" << zone.name() << "changed" << value; + changed = true; + } + if (changed) { updateZone(zone.id()); } } @@ -274,7 +293,7 @@ void AirConditioningManager::onThingStateChaged(Thing *thing, const StateTypeId void AirConditioningManager::onActionExecuted(const Action &action, Thing::ThingError status) { - if (action.triggeredBy() == Action::TriggeredByUser) { + if (action.triggeredBy() == Action::TriggeredByUser && status == Thing::ThingErrorNoError) { Thing *thing = m_thingManager->findConfiguredThing(action.thingId()); if (thing && thing->thingClass().interfaces().contains("thermostat")) { if (thing->thingClass().actionTypes().findById(action.actionTypeId()).name() == "targetTemperature") { @@ -353,46 +372,67 @@ void AirConditioningManager::updateZone(const QUuid &zoneId) qCDebug(dcAirConditioning()) << "Window open" << windowOpen << "Override active:" << overrideActive << "Time schedule active:" << timeScheduleActive << "target:" << targetTemp; - bool highHumidity = false; - bool badAir = false; + // To determine the zone temperature we'll first check the thermostats if they have a temp sensor and use the highest value + // If no thermstats with temp sensors are available, we'll use the highest temp value from the indoor sensors. + bool tempFromThermostat = false; + bool tempFromSensors = false; + double temperature = 0; + foreach (const ThingId &thingId, zone.thermostats()) { Thermostat *thermostat = m_thermostats.value(thingId); if (thermostat) { qCDebug(dcAirConditioning()) << "Setting window open" << windowOpen << " and target temp" << targetTemp; thermostat->setWindowOpen(windowOpen); thermostat->setTargetTemperature(targetTemp); + + if (thermostat->hasTemperatureSensor()) { + qCDebug(dcAirConditioning()) << "Thermostat has temperature sensor:" << thermostat->temperature(); + if (!tempFromThermostat) { + temperature = thermostat->temperature(); + tempFromThermostat = true; + } + temperature = qMax(temperature, thermostat->temperature()); + } } } + double humidity = 0; + uint voc = 0; + double pm25 = 0; + foreach (const ThingId &thingId, zone.indoorSensors()) { Thing *thing = m_thingManager->findConfiguredThing(thingId); - if (thing->thingClass().interfaces().contains("humiditysensor")) { - if (thing->stateValue("humidity").toDouble() >= 60) { // > 60 over longer periods of time may cause mould - highHumidity = true; + if (!tempFromThermostat) { + if (thing->thingClass().interfaces().contains("temperaturesensor")) { + if (!tempFromSensors) { + temperature = thing->stateValue("temperature").toDouble(); + tempFromSensors = true; + } else { + temperature = qMax(temperature, thing->stateValue("temperature").toDouble()); + } } } + if (thing->thingClass().interfaces().contains("humiditysensor")) { + humidity = qMax(humidity, thing->stateValue("humidity").toDouble()); + } + if (thing->thingClass().interfaces().contains("vocsensor")) { - if (thing->stateValue("voc").toDouble() >= 660) { // Moderate as of IAQ - badAir = true; - } + voc = qMax(voc, thing->stateValue("voc").toUInt()); } if (thing->thingClass().interfaces().contains("pm25sensor")) { - if (thing->stateValue("pm25").toDouble() >= 25) { // Moderate as of CAQI - badAir = true; - } + pm25 = qMax(pm25, thing->stateValue("pm25").toDouble()); } - } ZoneInfo::ZoneStatus newStatus = ZoneInfo::ZoneStatusFlagNone; newStatus.setFlag(ZoneInfo::ZoneStatusFlagWindowOpen, windowOpen); newStatus.setFlag(ZoneInfo::ZoneStatusFlagSetpointOverrideActive, overrideActive); newStatus.setFlag(ZoneInfo::ZoneStatusFlagTimeScheduleActive, timeScheduleActive); - newStatus.setFlag(ZoneInfo::ZoneStatusFlagHighHumidity, highHumidity); - newStatus.setFlag(ZoneInfo::ZoneStatusFlagBadAir, badAir); + newStatus.setFlag(ZoneInfo::ZoneStatusFlagHighHumidity, humidity >= 65); // > 60 over longer periods of time may cause mould, 70 will cause mould + newStatus.setFlag(ZoneInfo::ZoneStatusFlagBadAir, voc >= 660 || pm25 >= 25); // VOC: 660 Moderate as of IAQ, PM25: 25 Moderate as of CAQI if (zone.setpointOverrideMode() == ZoneInfo::SetpointOverrideModeEventual && newStatus != m_eventualOverrideCache.value(zone.id())) { @@ -403,11 +443,30 @@ void AirConditioningManager::updateZone(const QUuid &zoneId) } - if (targetTemp != zone.currentSetpoint() || newStatus != zone.zoneStatus()) { - qCDebug(dcAirConditioning()) << "Modifying Zone: setpoint:" << targetTemp << "status:" << newStatus; + if (targetTemp != zone.currentSetpoint() + || newStatus != zone.zoneStatus() + || temperature != zone.temperature() + || humidity != zone.humidity() + || voc != zone.voc() + || pm25 != zone.pm25() + ) { + qCDebug(dcAirConditioning()) << "Modifying Zone: setpoint:" << targetTemp << "status:" << newStatus << "temp:" << temperature << "humidity:" << humidity << "VOC:" << voc << "PM25:" << pm25; m_zones[zone.id()].setCurrentSetpoint(targetTemp); m_zones[zone.id()].setZoneStatus(newStatus); + m_zones[zone.id()].setTemperature(temperature); + m_zones[zone.id()].setHumidity(humidity); + m_zones[zone.id()].setVoc(voc); + m_zones[zone.id()].setPm25(pm25); emit zoneChanged(m_zones.value(zone.id())); + + foreach (const ThingId ¬ificationThingId, zone.notifications()) { + Notifications *notifications = m_notifications.value(notificationThingId); + if (!notifications) { + qCWarning(dcAirConditioning()) << "Stale notification thing id in zone!" << notificationThingId << m_notifications.keys(); + continue; + } + notifications->update(m_zones[zone.id()]); + } } } @@ -415,6 +474,7 @@ void AirConditioningManager::loadZones() { qCDebug(dcAirConditioning()) << "Loading zones"; QSettings settings(NymeaSettings::settingsPath() + "/airconditioning.conf", QSettings::IniFormat); + settings.beginGroup("zones"); qCDebug(dcAirConditioning()) << "child groups of zones" << settings.childKeys() << settings.childGroups(); foreach (const QString &key, settings.childGroups()) { @@ -447,8 +507,7 @@ void AirConditioningManager::loadZones() settings.endGroup(); // weekSchedule zone.setWeekSchedule(weekSchedule); - qCDebug(dcAirConditioning()) << "Loading thermostats" << settings.value("thermostats").toStringList() << settings.value("thermostats").toList(); - QList thermostats, windowSensors, indoorSensors, outdoorSensors; + QList thermostats, windowSensors, indoorSensors, outdoorSensors, notifications; foreach (const QString &thingId, settings.value("thermostats").toStringList()) { thermostats.append(ThingId(thingId)); } @@ -465,8 +524,12 @@ void AirConditioningManager::loadZones() outdoorSensors.append(ThingId(thingId)); } zone.setOutdoorSensors(outdoorSensors); + foreach (const QString &thingId, settings.value("notifications").toStringList()) { + notifications.append(ThingId(thingId)); + } + zone.setNotifications(notifications); - qCDebug(dcAirConditioning()) << "Zone Loaded:" << zone.thermostats(); + qCDebug(dcAirConditioning()) << "Zone Loaded:" << zone.thermostats() << zone.notifications(); m_zones.insert(zoneId, zone); settings.endGroup(); // zone } @@ -504,7 +567,7 @@ void AirConditioningManager::saveZones() } settings.endGroup(); // weekSchedule - QStringList thermostats, windowSensors, indoorSensors, outdoorSensors; + QStringList thermostats, windowSensors, indoorSensors, outdoorSensors, notifications; foreach (const ThingId &thingId, zone.thermostats()) { thermostats.append(thingId.toString()); } @@ -522,13 +585,18 @@ void AirConditioningManager::saveZones() } settings.setValue("outdoorSensors", outdoorSensors); + foreach (const ThingId &thingId, zone.notifications()) { + notifications.append(thingId.toString()); + } + settings.setValue("notifications", notifications); + settings.endGroup(); // zone } settings.endGroup(); } -AirConditioningManager::AirConditioningError AirConditioningManager::verifyThingIds(const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors) +AirConditioningManager::AirConditioningError AirConditioningManager::verifyThingIds(const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors, const QList ¬ifications) { foreach (const QUuid &thingId, thermostats) { Thing *thing = m_thingManager->findConfiguredThing(thingId); @@ -566,6 +634,17 @@ AirConditioningManager::AirConditioningError AirConditioningManager::verifyThing return AirConditioningErrorInvalidThingType; } } + foreach (const QUuid &thingId, notifications) { + Thing *thing = m_thingManager->findConfiguredThing(thingId); + if (!thing) { + qCWarning(dcAirConditioning()) << "No thing with id" << thingId; + return AirConditioningErrorThingNotFound; + } + if (!thing->thingClass().interfaces().contains("notifications")) { + qCWarning(dcAirConditioning()) << "Not a notification thing:" << thing->name(); + return AirConditioningErrorInvalidThingType; + } + } return AirConditioningErrorNoError; } diff --git a/airconditioningmanager.h b/airconditioningmanager.h index 8e2c514..83f0941 100644 --- a/airconditioningmanager.h +++ b/airconditioningmanager.h @@ -39,6 +39,7 @@ #include "zoneinfo.h" #include "thermostat.h" +#include "notifications.h" class AirConditioningManager : public QObject { @@ -56,8 +57,8 @@ public: explicit AirConditioningManager(ThingManager *thingManager, QObject *parent = nullptr); ZoneInfos zones() const; - ZoneInfo zone(const ThingId &thermostatId); - QPair addZone(const QString &name, const QList &thermostats, const QList windowSensors, const QList indoorSensors, const QList outdoorSensors); + ZoneInfo zone(const QUuid &thermostatId); + QPair addZone(const QString &name, const QList &thermostats, const QList windowSensors, const QList indoorSensors, const QList outdoorSensors, const QList notifications); AirConditioningError removeZone(const QUuid &zoneId); AirConditioningError setZoneName(const QUuid &zoneId, const QString &name); @@ -65,7 +66,7 @@ public: AirConditioningError setZoneSetpointOverride(const QUuid &zoneId, double setpoint, ZoneInfo::SetpointOverrideMode mode, uint minutes); AirConditioningError setZoneWeekSchedules(const QUuid &zoneId, const TemperatureWeekSchedule &temperatureWeekSchedule); - AirConditioningError setZoneThings(const QUuid &zoneId, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors); + AirConditioningError setZoneThings(const QUuid &zoneId, const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors, const QList ¬ifications); // AirConditioningError addThing(const QUuid &zoneId, const ThingId &thingId); // AirConditioningError removeThing(const QUuid &zoneId, const ThingId &thingId); @@ -74,6 +75,7 @@ signals: void zoneAdded(const ZoneInfo &zone); void zoneRemoved(const QUuid &zoneId); void zoneChanged(const ZoneInfo &zoneInfo); + void notificationThingsChanged(const QList ¬ificationThigns); private slots: void onThingAdded(Thing *thing); @@ -88,7 +90,7 @@ private: void loadZones(); void saveZones(); - AirConditioningError verifyThingIds(const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors); + AirConditioningError verifyThingIds(const QList &thermostats, const QList &windowSensors, const QList &indoorSensors, const QList &outdoorSensors, const QList ¬ifications); private: ThingManager *m_thingManager = nullptr; @@ -97,6 +99,7 @@ private: QHash m_thermostats; QHash m_zones; QHash m_eventualOverrideCache; + QHash m_notifications; QDateTime m_lastUpdateTime; }; diff --git a/notifications.cpp b/notifications.cpp new file mode 100644 index 0000000..5f05f91 --- /dev/null +++ b/notifications.cpp @@ -0,0 +1,101 @@ +#include "notifications.h" + +#include + +Notifications::Notifications(ThingManager *thingManager, Thing *thing, QObject *parent) + : QObject{parent}, + m_thingManager(thingManager), + m_thing(thing) +{ + m_clearTimer.setInterval(30*60*1000); + m_clearTimer.setSingleShot(true); + connect(&m_clearTimer, &QTimer::timeout, this, [this](){ + m_isShown = false; + }); +} + +void Notifications::update(const ZoneInfo &zone) +{ + bool supportsUpdate = m_thing->thingClassId() == ThingClassId("f0dd4c03-0aca-42cc-8f34-9902457b05de") + && m_thing->paramValue("service").toString() == "FB-GCM" + // Updating is only supported with versions that have the notificationId param + && !m_thing->thingClass().actionTypes().findByName("notify").paramTypes().findByName("notificationId").id().isNull(); + + QString notificationId = "humidityalert-" + zone.id().toString(); + QString title = "High humidity alert"; + QString text = QString("Humidity in zone %1: %2 %").arg(zone.name()).arg(zone.humidity()); + if (zone.zoneStatus().testFlag(ZoneInfo::ZoneStatusFlagHighHumidity)) { + if (!m_isShown) { + // show + updateNotification(notificationId, title, text, false, false); + } else if (supportsUpdate) { + // update + updateNotification(notificationId, title, text, false, false); + } + } else { + if (m_isShown && supportsUpdate) { + // remove + updateNotification(notificationId, title, text, false, true); + } + } + + notificationId = "airalert-" + zone.id().toString(); + title = "Bad air alert"; + text = QString("Bad air in zone %1: %2").arg(zone.name()); + QStringList airValues; + if (zone.voc() >= 660) { + airValues.append(QString("%1 ppm").arg(zone.voc())); + } + if (zone.pm25() >= 25) { + airValues.append(QString("%1 µg/m³").arg(zone.pm25())); + } + text = text.arg(airValues.join(",")); + if (zone.zoneStatus().testFlag(ZoneInfo::ZoneStatusFlagBadAir)) { + if (!m_isShown) { + // show + updateNotification(notificationId, title, text, false, false); + } else if (supportsUpdate) { + // update + updateNotification(notificationId, title, text, false, false); + } + } else { + if (m_isShown && supportsUpdate) { + // remove + updateNotification(notificationId, title, text, false, true); + } + } +} + +void Notifications::updateNotification(const QString &id, const QString &title, const QString &text, bool sound, bool remove) +{ + ActionType actionType = m_thing->thingClass().actionTypes().findByName("notify"); + Action action(actionType.id(), m_thing->id(), Action::TriggeredByRule); + + ParamList params = ParamList{ + Param(actionType.paramTypes().findByName("title").id(), title), + Param(actionType.paramTypes().findByName("body").id(), text), + }; + + if (m_thing->thingClassId() == ThingClassId("f0dd4c03-0aca-42cc-8f34-9902457b05de")) { + QUrlQuery data; + data.addQueryItem("open", "airconditioning"); + params.append(Param(actionType.paramTypes().findByName("data").id(), data.toString())); + + // For backwards compatibility, only add those if the plugin already has them + if (!m_thing->thingClass().actionTypes().findByName("notify").paramTypes().findByName("notificationId").id().isNull()) { + params.append(Param(actionType.paramTypes().findByName("notificationId").id(), id)); + params.append(Param(actionType.paramTypes().findByName("sound").id(), sound)); + params.append(Param(actionType.paramTypes().findByName("remove").id(), remove)); + } + } + action.setParams(params); + + ThingActionInfo *info = m_thingManager->executeAction(action); + connect(info, &ThingActionInfo::finished, this, [=](){ + if (info->status() == Thing::ThingErrorNoError) { + m_isShown = !remove; + m_clearTimer.start(); + } + }); + +} diff --git a/notifications.h b/notifications.h new file mode 100644 index 0000000..d5b4dac --- /dev/null +++ b/notifications.h @@ -0,0 +1,37 @@ +#ifndef NOTIFICATIONS_H +#define NOTIFICATIONS_H + +#include +#include + +#include +#include + +#include "zoneinfo.h" + + +class Notifications : public QObject +{ + Q_OBJECT +public: + explicit Notifications(ThingManager *thingManager, Thing *thing, QObject *parent = nullptr); + + void update(const ZoneInfo &zone); +signals: + +private: + void updateNotification(const QString &id, const QString &title, const QString &text, bool sound, bool remove); +private: + ThingManager *m_thingManager = nullptr; + Thing *m_thing = nullptr; + + ZoneInfo::ZoneStatus m_zoneStatus; + + bool m_isShown = false; + + // For devices that don't support updates/removals, we'll assume after some time that it's gone and we may need to show again + QTimer m_clearTimer; +}; + + +#endif // NOTIFICATIONS_H diff --git a/nymea-experience-plugin-airconditioning.pro b/nymea-experience-plugin-airconditioning.pro index e131c21..0dcec31 100644 --- a/nymea-experience-plugin-airconditioning.pro +++ b/nymea-experience-plugin-airconditioning.pro @@ -12,6 +12,7 @@ include(../config.pri) HEADERS += experiencepluginairconditioning.h \ airconditioningjsonhandler.h \ airconditioningmanager.h \ + notifications.h \ temperatureschedule.h \ thermostat.h \ zoneinfo.h @@ -19,6 +20,7 @@ HEADERS += experiencepluginairconditioning.h \ SOURCES += experiencepluginairconditioning.cpp \ airconditioningjsonhandler.cpp \ airconditioningmanager.cpp \ + notifications.cpp \ temperatureschedule.cpp \ thermostat.cpp \ zoneinfo.cpp diff --git a/thermostat.cpp b/thermostat.cpp index 2456b0c..ee394e3 100644 --- a/thermostat.cpp +++ b/thermostat.cpp @@ -128,3 +128,13 @@ void Thermostat::setWindowOpen(bool windowOpen) } } +bool Thermostat::hasTemperatureSensor() const +{ + return m_thing->thingClass().interfaces().contains("temperaturesensor"); +} + +double Thermostat::temperature() const +{ + return m_thing->stateValue("temperature").toDouble(); +} + diff --git a/thermostat.h b/thermostat.h index ee7b4cd..4383e42 100644 --- a/thermostat.h +++ b/thermostat.h @@ -46,6 +46,9 @@ public: void setTargetTemperature(double targetTemperature, bool force = false); void setWindowOpen(bool windowOpen); + bool hasTemperatureSensor() const; + double temperature() const; + signals: private: diff --git a/zoneinfo.cpp b/zoneinfo.cpp index add4624..65bff38 100644 --- a/zoneinfo.cpp +++ b/zoneinfo.cpp @@ -139,6 +139,16 @@ void ZoneInfo::setOutdoorSensors(const QList &outdoorSensors) m_outdoorSensors = outdoorSensors; } +QList ZoneInfo::notifications() const +{ + return m_notifications; +} + +void ZoneInfo::setNotifications(const QList ¬ifications) +{ + m_notifications = notifications; +} + ZoneInfo::ZoneStatus ZoneInfo::zoneStatus() const { return m_zoneStatus; @@ -154,6 +164,46 @@ void ZoneInfo::setZoneStatusFlag(ZoneStatusFlag flag, bool set) m_zoneStatus.setFlag(flag, set); } +double ZoneInfo::temperature() const +{ + return m_temperature; +} + +void ZoneInfo::setTemperature(double temperature) +{ + m_temperature = temperature; +} + +double ZoneInfo::humidity() const +{ + return m_humidity; +} + +void ZoneInfo::setHumidity(double humidity) +{ + m_humidity = humidity; +} + +uint ZoneInfo::voc() const +{ + return m_voc; +} + +void ZoneInfo::setVoc(uint voc) +{ + m_voc = voc; +} + +double ZoneInfo::pm25() const +{ + return m_pm25; +} + +void ZoneInfo::setPm25(double pm25) +{ + m_pm25 = pm25; +} + TemperatureWeekSchedule ZoneInfo::weekSchedule() const { return m_weekSchedule; diff --git a/zoneinfo.h b/zoneinfo.h index dc9b405..5aaa6ee 100644 --- a/zoneinfo.h +++ b/zoneinfo.h @@ -53,7 +53,12 @@ class ZoneInfo Q_PROPERTY(QList windowSensors READ windowSensors) Q_PROPERTY(QList indoorSensors READ indoorSensors) Q_PROPERTY(QList outdoorSensors READ outdoorSensors) + Q_PROPERTY(QList notifications READ notifications) Q_PROPERTY(ZoneStatus zoneStatus READ zoneStatus) + Q_PROPERTY(double temperature READ temperature) + Q_PROPERTY(double humidity READ humidity) + Q_PROPERTY(uint voc READ voc) + Q_PROPERTY(double pm25 READ pm25) Q_PROPERTY(TemperatureWeekSchedule weekSchedule READ weekSchedule) public: @@ -109,10 +114,25 @@ public: QList outdoorSensors() const; void setOutdoorSensors(const QList &outdoorSensors); + QList notifications() const; + void setNotifications(const QList ¬ifications); + ZoneInfo::ZoneStatus zoneStatus() const; void setZoneStatus(ZoneStatus zoneStatus); void setZoneStatusFlag(ZoneStatusFlag flag, bool set); + double temperature() const; + void setTemperature(double temperature); + + double humidity() const; + void setHumidity(double humidity); + + uint voc() const; + void setVoc(uint voc); + + double pm25() const; + void setPm25(double pm25); + TemperatureWeekSchedule weekSchedule() const; void setWeekSchedule(const TemperatureWeekSchedule &weekSchedule); @@ -128,7 +148,12 @@ private: QList m_windowSensors; QList m_indoorSensors; QList m_outdoorSensors; + QList m_notifications; ZoneStatus m_zoneStatus = ZoneStatusFlagNone; + double m_temperature = 0; + double m_humidity = 0; + uint m_voc = 0; + double m_pm25 = 0; TemperatureWeekSchedule m_weekSchedule; }; Q_DECLARE_METATYPE(ZoneInfo)