/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright 2013 - 2022, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. * This project including source code and documentation is protected by * copyright law, and remains the property of nymea GmbH. All rights, including * reproduction, publication, editing and translation, are reserved. The use of * this project is subject to the terms of a license agreement to be concluded * with nymea GmbH in accordance with the terms of use of nymea GmbH, available * under https://nymea.io/license * * GNU General Public License Usage * Alternatively, this project may be redistributed and/or modified under the * terms of the GNU General Public License as published by the Free Software * Foundation, GNU version 3. This project is distributed in the hope that it * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with * this project. If not, see . * * For any further details and any questions please contact us under * contact@nymea.io or see our FAQ/Licensing Information on * https://nymea.io/license/faq * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "airconditioningmanager.h" #include "zoneinfo.h" #include #include #include Q_DECLARE_LOGGING_CATEGORY(dcAirConditioning) AirConditioningManager::AirConditioningManager(ThingManager *thingManager, QObject *parent): QObject(parent), m_thingManager(thingManager) { qCDebug(dcAirConditioning()) << "Loading air conditioning experience..."; connect(m_thingManager, &ThingManager::thingAdded, this, &AirConditioningManager::onThingAdded); connect(m_thingManager, &ThingManager::thingRemoved, this, &AirConditioningManager::onThingRemoved); connect(m_thingManager, &ThingManager::thingStateChanged, this, &AirConditioningManager::onThingStateChaged); connect(m_thingManager, &ThingManager::actionExecuted, this, &AirConditioningManager::onActionExecuted); foreach (Thing *thing, m_thingManager->configuredThings()) { 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(); m_updateTimer = new QTimer(this); m_updateTimer->start(1000); connect(m_updateTimer, &QTimer::timeout, this, [=](){ if (m_lastUpdateTime.time().minute() != QDateTime::currentDateTime().time().minute()) { m_lastUpdateTime = QDateTime::currentDateTime(); update(); } }); } ZoneInfos AirConditioningManager::zones() const { return m_zones.values(); } ZoneInfo AirConditioningManager::zone(const QUuid &zoneId) { return m_zones.value(zoneId); } 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, notifications); if (status != AirConditioningErrorNoError) { qCWarning(dcAirConditioning()) << "Invalid thing id" << status << "in" << thermostats; return qMakePair(status, ZoneInfo()); } zone.setThermostats(thermostats); zone.setWindowSensors(windowSensors); zone.setIndoorSensors(indoorSensors); zone.setOutdoorSensors(outdoorSensors); zone.setNotifications(notifications); m_zones.insert(zone.id(), zone); saveZones(); emit zoneAdded(zone); return qMakePair(AirConditioningErrorNoError, zone); } AirConditioningManager::AirConditioningError AirConditioningManager::removeZone(const QUuid &zoneId) { if (!m_zones.contains(zoneId)) { return AirConditioningErrorZoneNotFound; } m_zones.remove(zoneId); saveZones(); emit zoneRemoved(zoneId); return AirConditioningErrorNoError; } AirConditioningManager::AirConditioningError AirConditioningManager::setZoneName(const QUuid &zoneId, const QString &name) { if (!m_zones.contains(zoneId)) { return AirConditioningErrorZoneNotFound; } m_zones[zoneId].setName(name); saveZones(); emit zoneChanged(m_zones.value(zoneId)); return AirConditioningErrorNoError; } AirConditioningManager::AirConditioningError AirConditioningManager::setZoneStandbySetpoint(const QUuid &zoneId, double standbySetpoint) { if (!m_zones.contains(zoneId)) { return AirConditioningErrorZoneNotFound; } m_zones[zoneId].setStandbySetpoint(standbySetpoint); saveZones(); emit zoneChanged(m_zones.value(zoneId)); update(); return AirConditioningErrorNoError; } AirConditioningManager::AirConditioningError AirConditioningManager::setZoneWeekSchedules(const QUuid &zoneId, const TemperatureWeekSchedule &weekSchedule) { if (!m_zones.contains(zoneId)) { return AirConditioningErrorZoneNotFound; } if (weekSchedule.count() != 7) { qCWarning(dcAirConditioning()) << "There must be exactly 7 schedules in a week schedule:" << weekSchedule; return AirConditioningErrorInvalidTimeSpec; } for (int day = 0; day < 7; day++) { TemperatureDaySchedule daySchedule = weekSchedule.at(day); for (int i = 0; i < daySchedule.count(); i++) { TemperatureSchedule schedule = daySchedule.at(i); if (schedule.startTime() >= schedule.endTime()) { qCWarning(dcAirConditioning()) << "Invalid time spec. startTime mus be earlier than endTime:" << schedule; return AirConditioningErrorInvalidTimeSpec; } for (int j = i+1; j < daySchedule.count(); j++) { TemperatureSchedule other = daySchedule.at(j); if (other.startTime() < schedule.startTime() && other.endTime() > schedule.startTime()) { qCWarning(dcAirConditioning()) << "Invalid time spec. Overlapping schedules:\n" << schedule << "\n" << other; return AirConditioningErrorInvalidTimeSpec; } if (other.startTime() < schedule.endTime() && other.endTime() > schedule.startTime()) { qCWarning(dcAirConditioning()) << "Invalid time spec. Overlapping schedules:\n" << schedule << "\n" << other; return AirConditioningErrorInvalidTimeSpec; } } } } m_zones[zoneId].setWeekSchedule(weekSchedule); saveZones(); emit zoneChanged(m_zones.value(zoneId)); qCInfo(dcAirConditioning()) << "Temperature schedule saved:" << weekSchedule; update(); return AirConditioningErrorNoError; } 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, notifications); if (status != AirConditioningErrorNoError) { return status; } m_zones[zoneId].setThermostats(thermostats); 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 << "notifications:" << notifications; emit zoneChanged(m_zones.value(zoneId)); updateZone(zoneId); return AirConditioningErrorNoError; } AirConditioningManager::AirConditioningError AirConditioningManager::setZoneSetpointOverride(const QUuid &zoneId, double setpoint, ZoneInfo::SetpointOverrideMode mode, uint minutes) { if (!m_zones.contains(zoneId)) { return AirConditioningErrorZoneNotFound; } m_zones[zoneId].setSetpointOverride(setpoint, mode, QDateTime::currentDateTime().addMSecs(minutes * 60000)); m_eventualOverrideCache[zoneId] = (m_zones[zoneId].zoneStatus() | ZoneInfo::ZoneStatusFlagSetpointOverrideActive); qCDebug(dcAirConditioning()) << "Memorizing zone status:" << m_eventualOverrideCache.value(zoneId); saveZones(); emit zoneChanged(m_zones.value(zoneId)); updateZone(zoneId); return AirConditioningErrorNoError; } void AirConditioningManager::onThingAdded(Thing *thing) { if (thing->thingClass().interfaces().contains("thermostat")) { 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) { foreach (const ZoneInfo &zone, m_zones) { QList thermostats = m_zones.value(zone.id()).thermostats(); 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); changed = true; } if (windowSensors.contains(thingId)) { windowSensors.removeAll(thingId); changed = true; } if (indoorSensors.contains(thingId)) { indoorSensors.removeAll(thingId); changed = true; } if (outdoorSensors.contains(thingId)) { outdoorSensors.removeAll(thingId); changed = true; } if (notifications.contains(thingId)) { notifications.removeAll(thingId); changed = true; } if (changed) { 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(minValue) Q_UNUSED(maxValue) StateType stateType = thing->thingClass().getStateType(stateTypeId); foreach (const ZoneInfo &zone, m_zones) { 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()); } } } void AirConditioningManager::onActionExecuted(const Action &action, Thing::ThingError status) { 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") { foreach (const ZoneInfo &zone, m_zones) { if (zone.thermostats().contains(thing->id())) { qCInfo(dcAirConditioning()).nospace() << "Target temperature changed on thermostat in zone " << zone.name() << ". Activating setpoint override for" << action.paramValue(action.actionTypeId()).toDouble(); m_zones[zone.id()].setSetpointOverride(action.paramValue(action.actionTypeId()).toDouble(), ZoneInfo::SetpointOverrideModeEventual); } } } } } } void AirConditioningManager::update() { qCDebug(dcAirConditioning()) << "Upadting air conditioning"; foreach (const QUuid &zoneId, m_zones.keys()) { updateZone(zoneId); } } void AirConditioningManager::updateZone(const QUuid &zoneId) { ZoneInfo zone = m_zones.value(zoneId); qCDebug(dcAirConditioning()) << "*** Evaluating Zone:" << zone.name(); QDateTime now = QDateTime::currentDateTime(); bool timeScheduleActive = false; bool overrideActive = false; qCDebug(dcAirConditioning()) << "Standby temp:" << zone.standbySetpoint() << "Override:" << zone.setpointOverrideMode() << zone.setpointOverride() << zone.setpointOverrideEnd().toString(Qt::DefaultLocaleShortDate) << "Schedules:" << zone.weekSchedule(); if (zone.setpointOverrideMode() == ZoneInfo::SetpointOverrideModeUnlimited || (zone.setpointOverrideMode() == ZoneInfo::SetpointOverrideModeTimed && zone.setpointOverrideEnd() > now) || zone.setpointOverrideMode() == ZoneInfo::SetpointOverrideModeEventual) { qCDebug(dcAirConditioning()) << "Setpoint override active until" << zone.setpointOverrideEnd(); overrideActive = true; } TemperatureDaySchedule daySchedule = zone.weekSchedule().at(now.date().dayOfWeek() - 1); double timeScheduleTemp; foreach (const TemperatureSchedule &schedule, daySchedule) { if (schedule.startTime() < now.time() && schedule.endTime() > now.time()) { qCDebug(dcAirConditioning()) << "Schedule is active:" << schedule; timeScheduleTemp = schedule.temperature(); timeScheduleActive = true; break; } } // Checking window open bool windowOpen = false; foreach (const ThingId &thingId, zone.thermostats()) { Thing *thing = m_thingManager->findConfiguredThing(thingId); if (!thing) { qCWarning(dcAirConditioning()) << "Thing" << thingId << "seems to have been removed from the system!"; continue; } if (thing->hasState("windowOpenDetected") && thing->stateValue("windowOpenDetected").toBool()) { windowOpen = true; break; } } foreach (const ThingId &thingId, zone.windowSensors()) { Thing *thing = m_thingManager->findConfiguredThing(thingId); if (!thing) { qCWarning(dcAirConditioning()) << "Thing" << thingId << "seems to have been removed from the system!"; continue; } if (!thing->stateValue("closed").toBool()) { qCInfo(dcAirConditioning()) << "Window open."; windowOpen = true; break; } } // *********** double targetTemp = zone.standbySetpoint(); if (overrideActive) { targetTemp = zone.setpointOverride(); } else if (timeScheduleActive) { targetTemp = timeScheduleTemp; } qCDebug(dcAirConditioning()) << "Window open" << windowOpen << "Override active:" << overrideActive << "Time schedule active:" << timeScheduleActive << "target:" << targetTemp; // 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 (!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")) { voc = qMax(voc, thing->stateValue("voc").toUInt()); } if (thing->thingClass().interfaces().contains("pm25sensor")) { 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, 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())) { qCDebug(dcAirConditioning()) << "Zone status changed:" << m_eventualOverrideCache.value(zone.id()) << "->" << newStatus << "Resetting eventual override"; m_zones[zone.id()].setSetpointOverride(zone.setpointOverride(), ZoneInfo::SetpointOverrideModeNone); updateZone(zone.id()); return; } 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()]); } } } 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()) { settings.beginGroup(key); qCDebug(dcAirConditioning()) << "Loading zone" << key; QUuid zoneId(key); ZoneInfo zone(zoneId); zone.setName(settings.value("name").toString()); QMetaEnum modeEnum = QMetaEnum::fromType(); ZoneInfo::SetpointOverrideMode mode = static_cast(modeEnum.keyToValue(settings.value("setpointOverrideMode", "SetpointOverrideModeNone").toByteArray())); zone.setSetpointOverride(settings.value("setpointOverride").toDouble(), mode, settings.value("setpointOverrideEnd").toDateTime()); zone.setStandbySetpoint(settings.value("standbySetpoint").toDouble()); settings.beginGroup("weekSchedule"); TemperatureWeekSchedule weekSchedule; for (int day = 0; day < 7; day++) { TemperatureDaySchedule daySchedule; settings.beginGroup(QString::number(day)); foreach (const QString &childGroup, settings.childGroups()) { settings.beginGroup(childGroup); QTime startTime = settings.value("startTime").toTime(); QTime endTime = settings.value("endTime").toTime(); double temperature = settings.value("temperature").toDouble(); TemperatureSchedule schedule(startTime, endTime, temperature); daySchedule.append(schedule); settings.endGroup(); // schedule } weekSchedule.append(daySchedule); settings.endGroup(); // daySchedule } settings.endGroup(); // weekSchedule zone.setWeekSchedule(weekSchedule); QList thermostats, windowSensors, indoorSensors, outdoorSensors, notifications; foreach (const QString &thingId, settings.value("thermostats").toStringList()) { thermostats.append(ThingId(thingId)); } zone.setThermostats(thermostats); foreach (const QString &thingId, settings.value("windowSensors").toStringList()) { windowSensors.append(ThingId(thingId)); } zone.setWindowSensors(windowSensors); foreach (const QString &thingId, settings.value("indoorSensors").toStringList()) { indoorSensors.append(ThingId(thingId)); } zone.setIndoorSensors(indoorSensors); foreach (const QString &thingId, settings.value("outdoorSensors").toStringList()) { 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() << zone.notifications(); m_zones.insert(zoneId, zone); settings.endGroup(); // zone } settings.endGroup(); // zones } void AirConditioningManager::saveZones() { qCDebug(dcAirConditioning()) << "Saving zones"; QSettings settings(NymeaSettings::settingsPath() + "/airconditioning.conf", QSettings::IniFormat); settings.beginGroup("zones"); settings.clear(); foreach (const ZoneInfo &zone, m_zones) { settings.beginGroup(zone.id().toString()); settings.setValue("name", zone.name()); settings.setValue("standbySetpoint", zone.standbySetpoint()); settings.setValue("setpointOverride", zone.setpointOverride()); QMetaEnum modeEnum = QMetaEnum::fromType(); settings.setValue("setpointOverrideMode", modeEnum.valueToKey(zone.setpointOverrideMode())); settings.setValue("setpointOverrideEnd", zone.setpointOverrideEnd()); settings.beginGroup("weekSchedule"); for (int day = 0; day < 7; day++) { settings.beginGroup(QString::number(day)); TemperatureDaySchedule daySchedule = zone.weekSchedule().at(day); for (int i = 0; i < daySchedule.count(); i++) { TemperatureSchedule schedule = daySchedule.at(i); settings.beginGroup(QString::number(i)); settings.setValue("startTime", schedule.startTime()); settings.setValue("endTime", schedule.endTime()); settings.setValue("temperature", schedule.temperature()); settings.endGroup(); // schedule } settings.endGroup(); // daySchedule } settings.endGroup(); // weekSchedule QStringList thermostats, windowSensors, indoorSensors, outdoorSensors, notifications; foreach (const ThingId &thingId, zone.thermostats()) { thermostats.append(thingId.toString()); } settings.setValue("thermostats", thermostats); foreach (const ThingId &thingId, zone.windowSensors()) { windowSensors.append(thingId.toString()); } settings.setValue("windowSensors", windowSensors); foreach (const ThingId &thingId, zone.indoorSensors()) { indoorSensors.append(thingId.toString()); } settings.setValue("indoorSensors", indoorSensors); foreach (const ThingId &thingId, zone.outdoorSensors()) { outdoorSensors.append(thingId.toString()); } 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, const QList ¬ifications) { foreach (const QUuid &thingId, thermostats) { Thing *thing = m_thingManager->findConfiguredThing(thingId); if (!thing) { qCWarning(dcAirConditioning()) << "No thing with id" << thingId; return AirConditioningErrorThingNotFound; } if (!thing->thingClass().interfaces().contains("thermostat")) { qCWarning(dcAirConditioning()) << "Not a thermostat:" << thing->name(); return AirConditioningErrorInvalidThingType; } } foreach (const QUuid &thingId, windowSensors) { Thing *thing = m_thingManager->findConfiguredThing(thingId); if (!thing) { qCWarning(dcAirConditioning()) << "No thing with id" << thingId; return AirConditioningErrorThingNotFound; } if (!thing->thingClass().interfaces().contains("closablesensor")) { qCWarning(dcAirConditioning()) << "Not a window sensor:" << thing->name(); return AirConditioningErrorInvalidThingType; } } foreach (const QUuid &thingId, indoorSensors + outdoorSensors) { Thing *thing = m_thingManager->findConfiguredThing(thingId); if (!thing) { qCWarning(dcAirConditioning()) << "No thing with id" << thingId; return AirConditioningErrorThingNotFound; } if (!thing->thingClass().interfaces().contains("temperaturesensor") && !thing->thingClass().interfaces().contains("humiditysensor") && !thing->thingClass().interfaces().contains("vocsensor") && !thing->thingClass().interfaces().contains("pm25sensor")) { qCWarning(dcAirConditioning()) << "Not a temperature, humidity, voc or pm25 sensor:" << thing->name(); 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; }