* Copyright (c) 2016-2025 dresden elektronik ingenieurtechnik gmbh.
* All rights reserved.
*
* The software in this package is published under the terms of the BSD
* style license a copy of which has been included with this distribution in
* the LICENSE.txt file.
*
*/
#include <QBuffer>
#include <QString>
#include <QVariantMap>
#include <QStringBuilder>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include "de_web_plugin.h"
#include "de_web_plugin_private.h"
#include "json.h"
#include "rest_alarmsystems.h"
#define MAX_RULES_COUNT 500
#define FAST_RULE_CHECK_INTERVAL_MS 10
#define NORMAL_RULE_CHECK_INTERVAL_MS 100
\param req - request data
\param rsp - response data
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::handleRulesApi(const ApiRequest &req, ApiResponse &rsp)
{
if ((req.path.size() == 3) && (req.hdr.method() == "GET") && (req.path[2] == "rules"))
{
return getAllRules(req, rsp);
}
else if ((req.path.size() == 4) && (req.hdr.method() == "GET") && (req.path[2] == "rules"))
{
return getRule(req, rsp);
}
else if ((req.path.size() == 3) && (req.hdr.method() == "POST") && (req.path[2] == "rules"))
{
return createRule(req, rsp);
}
else if ((req.path.size() == 4) && (req.hdr.method() == "PUT" || req.hdr.method() == "PATCH") && (req.path[2] == "rules"))
{
return updateRule(req, rsp);
}
else if ((req.path.size() == 4) && (req.hdr.method() == "DELETE") && (req.path[2] == "rules"))
{
return deleteRule(req, rsp);
}
return REQ_NOT_HANDLED;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::getAllRules(const ApiRequest &req, ApiResponse &rsp)
{
Q_UNUSED(req)
rsp.httpStatus = HttpStatusOk;
auto i = rules.cbegin();
const auto end = rules.cend();
for (; i != end; ++i)
{
if (i->state() == Rule::StateDeleted)
{
continue;
}
QVariantMap rule;
auto c = i->conditions().cbegin();
const auto cend = i->conditions().cend();
QVariantList conditions;
for (; c != cend; ++c)
{
QVariantMap condition;
condition["address"] = c->address();
condition["operator"] = c->ooperator();
if (c->value().isValid())
{
condition["value"] = c->value().toString();
}
conditions.append(condition);
}
auto a = i->actions().cbegin();
const auto aend = i->actions().cend();
QVariantList actions;
for (; a != aend; ++a)
{
QVariantMap action;
action["address"] = a->address();
action["method"] = a->method();
bool ok;
QVariant body = Json::parse(a->body(), ok);
if (ok)
{
action["body"] = body;
actions.append(action);
}
}
rule["name"] = i->name();
if (i->lastTriggered().isValid())
{
rule["lasttriggered"] = i->lastTriggered().toString(QLatin1String("yyyy-MM-ddTHH:mm:ss"));
}
else
{
rule["lasttriggered"] = QLatin1String("none");
}
rule["created"] = i->creationtime();
rule["timestriggered"] = i->timesTriggered();
rule["owner"] = i->owner();
rule["status"] = i->status();
rule["conditions"] = conditions;
rule["actions"] = actions;
rule["periodic"] = static_cast<double>(i->triggerPeriodic());
QString etag = i->etag;
etag.remove('"');
rule["etag"] = etag;
rsp.map[i->id()] = rule;
}
if (rsp.map.isEmpty())
{
rsp.str = "{}";
}
return REQ_READY_SEND;
}
\return true - on success
false - on error
*/
bool DeRestPluginPrivate::ruleToMap(const Rule *rule, QVariantMap &map)
{
if (!rule)
{
return false;
}
auto c = rule->conditions().cbegin();
const auto c_end = rule->conditions().cend();
QVariantList conditions;
for (; c != c_end; ++c)
{
QVariantMap condition;
condition["address"] = c->address();
condition["operator"] = c->ooperator();
if (c->value().isValid())
{
condition["value"] = c->value().toString();
}
conditions.append(condition);
}
auto a = rule->actions().cbegin();
const auto a_end = rule->actions().cend();
QVariantList actions;
for (; a != a_end; ++a)
{
QVariantMap action;
action["address"] = a->address();
action["method"] = a->method();
bool ok;
QVariant body = Json::parse(a->body(), ok);
QVariantMap bodymap = body.toMap();
auto b = bodymap.cbegin();
const auto b_end = bodymap.cend();
QVariantMap resultmap;
for (; b != b_end; ++b)
{
resultmap[b.key()] = b.value();
}
action["body"] = resultmap;
actions.append(action);
}
map["actions"] = actions;
map["conditions"] = conditions;
map["created"] = rule->creationtime();
if (rule->lastTriggered().isValid())
{
map["lasttriggered"] = rule->lastTriggered().toString(QLatin1String("yyyy-MM-ddTHH:mm:ss"));
}
else
{
map["lasttriggered"] = QLatin1String("none");
}
map["name"] = rule->name();
map["owner"] = rule->owner();
map["periodic"] = rule->triggerPeriodic();
map["status"] = rule->status();
map["timestriggered"] = rule->timesTriggered();
QString etag = rule->etag;
etag.remove('"');
map["etag"] = etag;
return true;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::getRule(const ApiRequest &req, ApiResponse &rsp)
{
DBG_Assert(req.path.size() == 4);
if (req.path.size() != 4)
{
return -1;
}
const QString &id = req.path[3];
Rule *rule = getRuleForId(id);
if (!rule || (rule->state() == Rule::StateDeleted))
{
rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, QString("/rules/%1").arg(id), QString("resource, /rules/%1, not available").arg(id)));
rsp.httpStatus = HttpStatusNotFound;
return REQ_READY_SEND;
}
auto c = rule->conditions().cbegin();
const auto c_end = rule->conditions().cend();
QVariantList conditions;
for (; c != c_end; ++c)
{
QVariantMap condition;
condition["address"] = c->address();
condition["operator"] = c->ooperator();
if (c->value().isValid())
{
condition["value"] = c->value().toString();
}
conditions.append(condition);
}
auto a = rule->actions().cbegin();
const auto a_end = rule->actions().cend();
QVariantList actions;
for (; a != a_end; ++a)
{
QVariantMap action;
action["address"] = a->address();
action["method"] = a->method();
bool ok;
QVariant body = Json::parse(a->body(), ok);
QVariantMap bodymap = body.toMap();
auto b = bodymap.cbegin();
const auto b_end = bodymap.cend();
QVariantMap resultmap;
for (; b != b_end; ++b)
{
resultmap[b.key()] = b.value();
}
action["body"] = resultmap;
actions.append(action);
}
rsp.map["name"] = rule->name();
if (rule->lastTriggered().isValid())
{
rsp.map["lasttriggered"] = rule->lastTriggered().toString("yyyy-MM-ddTHH:mm:ss");
}
else
{
rsp.map["lasttriggered"] = QLatin1String("none");
}
rsp.map["created"] = rule->creationtime();
rsp.map["timestriggered"] = rule->timesTriggered();
rsp.map["owner"] = rule->owner();
rsp.map["status"] = rule->status();
rsp.map["conditions"] = conditions;
rsp.map["actions"] = actions;
rsp.map["periodic"] = static_cast<double>(rule->triggerPeriodic());
QString etag = rule->etag;
etag.remove('"');
rsp.map["etag"] = etag;
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::createRule(const ApiRequest &req, ApiResponse &rsp)
{
bool error = false;
rsp.httpStatus = HttpStatusOk;
const QString &apikey = req.path[1];
bool ok;
Rule rule;
QVariant var = Json::parse(req.content, ok);
QVariantMap map = var.toMap();
QVariantList conditionsList = map["conditions"].toList();
QVariantList actionsList = map["actions"].toList();
if (!ok)
{
rsp.list.append(errorToMap(ERR_INVALID_JSON, QString("/rules"), QString("body contains invalid JSON")));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
userActivity();
if (rules.size() >= MAX_RULES_COUNT) //deletet rules will be count
{
rsp.list.append(errorToMap(ERR_RULE_ENGINE_FULL , QString("/rules/"), QString("The Rule Engine has reached its maximum capacity of %1 rules").arg(MAX_RULES_COUNT)));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
*/
if (!map.contains("name"))
{
error = true;
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/rules/name"), QString("invalid/missing parameters in body")));
}
if (conditionsList.size() < 1)
{
error = true;
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/rules/conditions"), QString("invalid/missing parameters in body")));
}
if (actionsList.size() < 1)
{
error = true;
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/rules/actions"), QString("invalid/missing parameters in body")));
}
if (conditionsList.size() > 8)
{
error = true;
rsp.list.append(errorToMap(ERR_TOO_MANY_ITEMS, QString("/rules/conditions"), QString("too many items in list")));
}
if (actionsList.size() > 8)
{
error = true;
rsp.list.append(errorToMap(ERR_TOO_MANY_ITEMS, QString("/rules/actions"), QString("too many items in list")));
}
if (map.contains("status"))
{
QString status = map["status"].toString();
if (!(status == "disabled" || status == "enabled"))
{
error = true;
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/status"), QString("invalid value, %1, for parameter, status").arg(status)));
}
}
if (map.contains("periodic"))
{
int periodic = map["periodic"].toInt(&ok);
if (!ok)
{
error = true;
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/periodic"), QString("invalid value, %1, for parameter, peridoc").arg(map["periodic"].toString())));
}
else
{
rule.setTriggerPeriodic(periodic);
}
}
if (error)
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
else
{
QString name = map["name"].toString();
if ((map["name"].type() == QVariant::String) && !name.isEmpty())
{
if (name.size() > MAX_RULE_NAME_LENGTH)
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/name"), QString("invalid/missing parameters in body")));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
QVariantMap rspItem;
QVariantMap rspItemState;
rule.setId("1");
do {
ok = true;
auto i = rules.cbegin();
const auto end = rules.cend();
for (; i != end; ++i)
{
if (i->id() == rule.id())
{
rule.setId(QString::number(i->id().toInt() + 1));
ok = false;
}
}
} while (!ok);
rule.setName(name);
rule.setOwner(apikey);
rule.setCreationtime(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH:mm:ss"));
if (map.contains("status"))
{
rule.setStatus(map["status"].toString());
}
if (checkActions(actionsList, rsp))
{
std::vector<RuleAction> actions;
auto ai = actionsList.cbegin();
const auto aend = actionsList.cend();
for (; ai != aend; ++ai)
{
RuleAction newAction;
const QVariantMap m = ai->toMap();
newAction.setAddress(m["address"].toString());
newAction.setBody(Json::serialize(m["body"].toMap()));
newAction.setMethod(m["method"].toString());
actions.push_back(newAction);
}
rule.setActions(actions);
}
else
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
if (checkConditions(conditionsList, rsp))
{
std::vector<RuleCondition> conditions;
auto ci = conditionsList.cbegin();
const auto cend = conditionsList.cend();
for (; ci != cend; ++ci)
{
const RuleCondition cond(ci->toMap());
if (cond.isValid())
{
conditions.push_back(cond);
}
}
rule.setConditions(conditions);
}
else
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
updateEtag(rule.etag);
updateEtag(gwConfigEtag);
DBG_Printf(DBG_INFO, "create rule %s: %s\n", qPrintable(rule.id()), qPrintable(rule.name()));
rules.push_back(rule);
rules.back().setNeedSaveDatabase();
indexRulesTriggers();
queSaveDb(DB_RULES, DB_SHORT_SAVE_DELAY);
rspItemState["id"] = rule.id();
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
else
{
rsp.list.append(errorToMap(ERR_INVALID_JSON, QString("/rules"), QString("body contains invalid JSON")));
rsp.httpStatus = HttpStatusBadRequest;
}
}
return REQ_READY_SEND;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::updateRule(const ApiRequest &req, ApiResponse &rsp)
{
bool ok;
bool error = false;
bool changed = false;
QString id = req.path[3];
Rule *rule = getRuleForId(id);
if (!rule || (rule->state() == Rule::StateDeleted))
{
rsp.httpStatus = HttpStatusNotFound;
rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, QString("/rules/%1").arg(id), QString("resource, /rules/%1, not available").arg(id)));
return REQ_READY_SEND;
}
DBG_Printf(DBG_INFO, "update rule %s: %s\n", qPrintable(id), qPrintable(rule->name()));
QVariant var = Json::parse(req.content, ok);
QVariantMap map = var.toMap();
QVariantList conditionsList;
QVariantList actionsList;
QString name;
QString status;
int periodic = 0;
rsp.httpStatus = HttpStatusOk;
if (!ok)
{
rsp.list.append(errorToMap(ERR_INVALID_JSON, QString("/rules"), QString("body contains invalid JSON")));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
userActivity();
auto pi = map.cbegin();
const auto pend = map.cend();
for (; pi != pend; ++pi)
{
if(!((pi.key() == QLatin1String("name")) || (pi.key() == QLatin1String("status")) || (pi.key() == QLatin1String("actions")) || (pi.key() == QLatin1String("conditions")) || (pi.key() == QLatin1String("periodic"))))
{
rsp.list.append(errorToMap(ERR_PARAMETER_NOT_AVAILABLE, QString("/rules/%1/%2").arg(id).arg(pi.key()), QString("parameter, %1, not available").arg(pi.key())));
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
}
if (map.contains("name"))
{
name = map["name"].toString();
if ((map["name"].type() == QVariant::String) && !(name.isEmpty()))
{
if (name.size() > MAX_RULE_NAME_LENGTH)
{
error = true;
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/%1/name").arg(id), QString("invalid value, %1, for parameter, /rules/%2/name").arg(name).arg(id)));
rsp.httpStatus = HttpStatusBadRequest;
name = QString();
}
}
else
{
error = true;
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/%1/name").arg(id), QString("invalid value, %1, for parameter, /rules/%2/name").arg(name).arg(id)));
rsp.httpStatus = HttpStatusBadRequest;
name = QString();
}
}
if (map.contains("conditions"))
{
conditionsList = map["conditions"].toList();
if (conditionsList.size() < 1)
{
error = true;
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/rules/conditions"), QString("invalid/missing parameters in body")));
}
if (conditionsList.size() > 8)
{
error = true;
rsp.list.append(errorToMap(ERR_TOO_MANY_ITEMS, QString("/rules/conditions"), QString("too many items in list")));
}
}
if (map.contains("actions"))
{
actionsList = map["actions"].toList();
if (actionsList.size() < 1)
{
error = true;
rsp.list.append(errorToMap(ERR_MISSING_PARAMETER, QString("/rules/actions"), QString("invalid/missing parameters in body")));
}
if (actionsList.size() > 8)
{
error = true;
rsp.list.append(errorToMap(ERR_TOO_MANY_ITEMS, QString("/rules/actions"), QString("too many items in list")));
}
}
if (map.contains("status"))
{
status = map["status"].toString();
if (!(status == "disabled" || status == "enabled"))
{
error = true;
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/status"), QString("invalid value, %1, for parameter, status").arg(status)));
}
}
if (map.contains("periodic"))
{
periodic = map["periodic"].toInt(&ok);
if (!ok)
{
error = true;
rsp.list.append(errorToMap(ERR_INVALID_VALUE, QString("/rules/periodic"), QString("invalid value, %1, for parameter, peridoc").arg(map["periodic"].toString())));
}
}
if (error)
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
if (map.contains("actions") || map.contains("conditions"))
{
rule->setStatus("disabled");
}
if (!name.isEmpty())
{
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState[QString("/rules/%1/name").arg(id)] = name;
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
if (rule->name() != name)
{
changed = true;
rule->setName(name);
}
}
if (map.contains("status"))
{
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState[QString("/rules/%1/status").arg(id)] = status;
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
if (rule->status() != status)
{
changed = true;
rule->setStatus(status);
}
}
if (map.contains("periodic"))
{
if (rule->triggerPeriodic() != periodic)
{
changed = true;
rule->setTriggerPeriodic(periodic);
}
}
if (map.contains("actions"))
{
changed = true;
if (checkActions(actionsList,rsp))
{
std::vector<RuleAction> actions;
auto ai = actionsList.cbegin();
const auto aend = actionsList.cend();
for (; ai != aend; ++ai)
{
RuleAction newAction;
newAction.setAddress(ai->toMap()["address"].toString());
newAction.setBody(Json::serialize(ai->toMap()["body"].toMap()));
newAction.setMethod(ai->toMap()["method"].toString());
actions.push_back(newAction);
}
rule->setActions(actions);
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState[QString("/rules/%1/actions").arg(id)] = actionsList;
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
}
else
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
}
if (map.contains("conditions"))
{
changed = true;
if (checkConditions(conditionsList, rsp))
{
std::vector<RuleCondition> conditions;
auto ci = conditionsList.cbegin();
const auto cend = conditionsList.cend();
for (; ci != cend; ++ci)
{
const RuleCondition cond(ci->toMap());
if (cond.isValid())
{
conditions.push_back(cond);
}
}
rule->setConditions(conditions);
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState[QString("/rules/%1/conditions").arg(id)] = conditionsList;
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
indexRulesTriggers();
}
else
{
rsp.httpStatus = HttpStatusBadRequest;
return REQ_READY_SEND;
}
}
if (!map.contains("status"))
{
rule->setStatus("enabled");
}
if (changed)
{
updateEtag(rule->etag);
updateEtag(gwConfigEtag);
rule->setNeedSaveDatabase();
queSaveDb(DB_RULES, DB_SHORT_SAVE_DELAY);
}
return REQ_READY_SEND;
}
\param actionsList the actionsList
*/
bool DeRestPluginPrivate::checkActions(QVariantList actionsList, ApiResponse &rsp)
{
auto ai = actionsList.cbegin();
const auto aend = actionsList.cend();
for (; ai != aend; ++ai)
{
QString address = ai->toMap()["address"].toString();
QString method = ai->toMap()["method"].toString();
QString body = ai->toMap()["body"].toString();
QStringList addrList = ai->toMap()["address"].toString().split('/', SKIP_EMPTY_PARTS);
if (addrList.size() < 2)
{
rsp.list.append(errorToMap(ERR_ACTION_ERROR, QString(address),
QString("Rule actions contain errors or an action on a unsupported resource")));
return false;
}
const char *resources[] = { "groups", "lights", "schedules", "scenes", "sensors", "rules", "alarmsystems", nullptr };
for (int i = 0; ; i++)
{
if (address.startsWith(QLatin1String("http")))
{
break;
}
if (!resources[i])
{
rsp.list.append(errorToMap(ERR_ACTION_ERROR, address, QLatin1String("Rule actions contain errors or an action on a unsupported resource")));
return false;
}
if (addrList[0] == QLatin1String(resources[i]))
{
break;
}
}
if(!(method == QLatin1String("PUT") || method == QLatin1String("POST") || method == QLatin1String("DELETE") || method == QLatin1String("GET")))
{
rsp.list.append(errorToMap(ERR_INVALID_VALUE , QLatin1String("rules/method"), QString("invalid value, %1, for parameter, method").arg(method)));
return false;
}
bool ok;
Json::parse(body, ok);
if (!ok)
{
rsp.list.append(errorToMap(ERR_INVALID_JSON, QLatin1String("/rules/"), QLatin1String("body contains invalid JSON")));
return false;
}
}
return true;
}
\param conditionsList the conditionsList
*/
bool DeRestPluginPrivate::checkConditions(QVariantList conditionsList, ApiResponse &rsp)
{
auto ci = conditionsList.cbegin();
const auto cend = conditionsList.cend();
for (; ci != cend; ++ci)
{
const RuleCondition cond(ci->toMap());
Resource *resource = cond.isValid() ? getResource(cond.resource(), cond.id()) : nullptr;
ResourceItem *item = resource ? resource->item(cond.suffix()) : nullptr;
if (!resource || !item)
{
rsp.list.append(errorToMap(ERR_CONDITION_ERROR, QString(cond.address()), QLatin1String("Condition error")));
return false;
}
}
return true;
}
\return REQ_READY_SEND
REQ_NOT_HANDLED
*/
int DeRestPluginPrivate::deleteRule(const ApiRequest &req, ApiResponse &rsp)
{
QString id = req.path[3];
Rule *rule = getRuleForId(id);
userActivity();
if (!rule || (rule->state() == Rule::StateDeleted))
{
rsp.httpStatus = HttpStatusNotFound;
rsp.list.append(errorToMap(ERR_RESOURCE_NOT_AVAILABLE, QString("/rules/%1").arg(id), QString("resource, /rules/%1, not available").arg(id)));
return REQ_READY_SEND;
}
rule->setState(Rule::StateDeleted);
rule->setStatus("disabled");
DBG_Printf(DBG_INFO, "delete rule %s: %s\n", qPrintable(id), qPrintable(rule->name()));
QVariantMap rspItem;
QVariantMap rspItemState;
rspItemState["id"] = id;
rspItem["success"] = rspItemState;
rsp.list.append(rspItem);
rsp.httpStatus = HttpStatusOk;
updateEtag(gwConfigEtag);
updateEtag(rule->etag);
rule->setNeedSaveDatabase();
queSaveDb(DB_RULES, DB_SHORT_SAVE_DELAY);
rsp.httpStatus = HttpStatusOk;
return REQ_READY_SEND;
}
\param rule - the rule to check
\param e - the trigger event
\param eResource - the event resource
\param eItem - the event resource item
\param now - the current date/time to check the rule against
\return true if rule can be triggered
*/
bool DeRestPluginPrivate::evaluateRule(Rule &rule, const Event &e, Resource *eResource, ResourceItem *eItem, QDateTime now, QDateTime previousNow)
{
if (!apsCtrl || !eItem || !eResource || (apsCtrl->networkState() != deCONZ::InNetwork))
{
return false;
}
if (rule.state() != Rule::StateNormal || !rule.isEnabled())
{
return false;
}
if (rule.triggerPeriodic() < 0)
{
return false;
}
if (rule.triggerPeriodic() > 0)
{
if (rule.lastTriggered().isValid() &&
rule.lastTriggered().addMSecs(rule.triggerPeriodic()) > now)
{
return false;
}
}
auto c = rule.conditions().cbegin();
const auto cend = rule.conditions().cend();
for (; c != cend; ++c)
{
Resource *resource = getResource(c->resource(), c->id());
ResourceItem *item = resource ? resource->item(c->suffix()) : nullptr;
Resource *valueResource = c->valueResource() ? getResource(c->valueResource(), c->valueId()) : nullptr;
ResourceItem *valueItem = valueResource ? valueResource->item(c->valueSuffix()) : nullptr;
if (!resource || !item)
{
DBG_Printf(DBG_INFO, "rule: %s, resource %s : %s id: %s (cond: %s) not found\n",
qPrintable(rule.id()), c->resource(), c->suffix(),
qPrintable(c->id()), qPrintable(c->address()));
if (!resource)
{
DBG_Printf(DBG_INFO, "\tdisable rule %s: %s\n", qPrintable(rule.id()), qPrintable(rule.name()));
rule.setStatus(QLatin1String("disabled"));
}
return false;
}
if (!item->lastSet().isValid()) { return false; }
if (resource->prefix() == RSensors && c->suffix() != RConfigOn)
{
ResourceItem *item2 = resource->item(RConfigOn);
if (item2 && !item2->toBool())
{
return false;
}
}
if (c->op() == RuleCondition::OpEqual)
{
if (c->numericValue() != item->toNumber())
{
return false;
}
if (item == eItem && e.num() == e.numPrevious())
{
return false;
}
}
else if (c->op() == RuleCondition::OpNotEqual)
{
if (c->numericValue() == item->toNumber())
{
return false;
}
if (item == eItem && e.num() == e.numPrevious())
{
return false;
}
}
else if (c->op() == RuleCondition::OpGreaterThan && item->descriptor().suffix == RStateLocaltime)
{
if (valueItem && valueItem->descriptor().suffix == RStateLocaltime)
{
if (valueItem->toNumber() < item->toNumber())
{
return false;
}
}
else if (valueItem && valueItem->descriptor().suffix == RConfigLocalTime)
{
const QDateTime t1 = QDateTime::fromMSecsSinceEpoch(item->toNumber());
if (now.time() < t1.time())
{
return false;
}
}
}
else if (c->op() == RuleCondition::OpLowerThan && item->descriptor().suffix == RStateLocaltime)
{
if (valueItem && valueItem->descriptor().suffix == RStateLocaltime)
{
if (valueItem->toNumber() > item->toNumber())
{
return false;
}
}
else if (valueItem && valueItem->descriptor().suffix == RConfigLocalTime)
{
const QDateTime t1 = QDateTime::fromMSecsSinceEpoch(item->toNumber());
if (now.time() > t1.time())
{
return false;
}
}
}
else if (c->op() == RuleCondition::OpGreaterThan)
{
if (item->toNumber() <= c->numericValue())
{
return false;
}
if (item == eItem && e.numPrevious() > c->numericValue())
{
return false;
}
}
else if (c->op() == RuleCondition::OpLowerThan)
{
if (item->toNumber() >= c->numericValue())
{
return false;
}
if (item == eItem && e.numPrevious() < c->numericValue())
{
return false;
}
}
else if (c->op() == RuleCondition::OpDx)
{
if (item != eItem)
{
return false;
}
if (eItem->descriptor().suffix == RStateLastUpdated)
{}
else if (eItem->descriptor().suffix == RAttrLastAnnounced)
{}
else if (eItem->descriptor().suffix == RConfigLocalTime)
{}
else if (e.num() == e.numPrevious())
{
return false;
}
}
else if (c->op() == RuleCondition::OpDdx)
{
if (eItem->descriptor().suffix != RConfigLocalTime)
{
return false;
}
if (!item->lastChanged().isValid())
{
return false;
}
QDateTime dt = item->lastChanged().addSecs(c->seconds());
if (dt <= previousNow || dt > now)
{
return false;
}
}
else if (c->op() == RuleCondition::OpStable)
{
if (!item->lastSet().isValid())
{
return false;
}
QDateTime dt = item->lastChanged().addSecs(c->seconds());
if (now.secsTo(dt) > 0)
{
return false;
}
}
else if (c->op() == RuleCondition::OpIn && c->suffix() == RConfigLocalTime)
{
const QTime t = now.time();
const QTime pt = previousNow.time();
if (eItem->descriptor().suffix == RConfigLocalTime && (c->time0() <= pt || c->time0() > t))
{
return false;
}
if (!c->weekDayEnabled(now.date().dayOfWeek()))
{
return false;
}
if (c->time0() < c->time1() &&
(t >= c->time0() && t <= c->time1()))
{
}
else if (c->time0() > c->time1() &&
(t >= c->time0() || t <= c->time1()))
{
}
else
{
return false;
}
}
else if (c->op() == RuleCondition::OpNotIn && c->suffix() == RConfigLocalTime)
{
const QTime t = now.time();
const QTime pt = previousNow.time();
if (eItem->descriptor().suffix == RConfigLocalTime && (c->time1() <= pt || c->time1() > t))
{
return false;
}
if (!c->weekDayEnabled(now.date().dayOfWeek()))
{
return false;
}
if (c->time0() < c->time1() &&
(t <= c->time0() || t >= c->time1()))
{
}
else if (c->time0() > c->time1() &&
(t <= c->time0() && t >= c->time1()))
{
}
else
{
return false;
}
}
else
{
DBG_Printf(DBG_ERROR, "error: rule (%s) operator %s not supported\n", qPrintable(rule.id()), qPrintable(c->ooperator()));
return false;
}
}
return true;
}
\param rule - the rule to index
*/
void DeRestPluginPrivate::indexRuleTriggers(Rule &rule)
{
ResourceItem *itemDx = nullptr;
ResourceItem *itemDdx = nullptr;
std::vector<ResourceItem*> items;
for (const RuleCondition &c : rule.conditions())
{
Resource *resource = getResource(c.resource(), c.id());
ResourceItem *item = resource ? resource->item(c.suffix()) : nullptr;
if (!resource || !item)
{
continue;
}
if (!c.id().isEmpty())
{
DBG_Printf(DBG_INFO_L2, "\t%s/%s/%s op: %s\n", c.resource(), qPrintable(c.id()), c.suffix(), qPrintable(c.ooperator()));
}
else
{
DBG_Printf(DBG_INFO_L2, "\t%s : %s op: %s\n", c.resource(), c.suffix(), qPrintable(c.ooperator()));
}
if (c.op() == RuleCondition::OpDx)
{
DBG_Assert(itemDx == nullptr);
DBG_Assert(itemDdx == nullptr);
itemDx = item;
}
else if (c.op() == RuleCondition::OpDdx)
{
DBG_Assert(itemDx == nullptr);
DBG_Assert(itemDdx == nullptr);
itemDdx = item;
}
else if (c.op() == RuleCondition::OpStable) { }
else if (c.op() == RuleCondition::OpNotStable) { }
else
{
items.push_back(item);
}
}
if (itemDx)
{
items.clear();
items.push_back(itemDx);
}
else if (itemDdx)
{
Resource *r = getResource(RConfig);
itemDdx = r ? r->item(RConfigLocalTime) : nullptr;
DBG_Assert(r != nullptr);
DBG_Assert(itemDdx != nullptr);
items.clear();
if (itemDdx)
{
items.push_back(itemDdx);
}
}
for (ResourceItem *item : items)
{
item->inRule(rule.handle());
DBG_Printf(DBG_INFO_L2, "\t%s (trigger)\n", item->descriptor().suffix);
}
}
\param rule - the rule to trigger
*/
void DeRestPluginPrivate::triggerRule(Rule &rule)
{
if (rule.state() != Rule::StateNormal || !rule.isEnabled())
{
return;
}
DBG_Printf(DBG_INFO, "trigger rule %s - %s\n", qPrintable(rule.id()), qPrintable(rule.name()));
bool triggered = false;
auto ai = rule.actions().cbegin();
const auto aend = rule.actions().cend();
for (; ai != aend; ++ai)
{
if (ai->address().startsWith(QLatin1String("http")))
{
if (handleWebHook(*ai) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
continue;
}
if (ai->method() != QLatin1String("PUT") && ai->method() != QLatin1String("POST"))
return;
QStringList path = ai->address().split(QChar('/'), SKIP_EMPTY_PARTS);
if (path.isEmpty())
return;
QHttpRequestHeader hdr(ai->method(), ai->address());
path.prepend(rule.owner());
path.prepend(QLatin1String("api"));
ApiRequest req(hdr, path, nullptr, ai->body());
ApiResponse rsp;
rsp.httpStatus = HttpStatusServiceUnavailable;
if (path[2] == QLatin1String("groups"))
{
if (handleGroupsApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("lights"))
{
if (handleLightsApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("schedules"))
{
if (handleSchedulesApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("scenes"))
{
if (handleScenesApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("sensors"))
{
if (handleSensorsApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("config"))
{
if (handleConfigFullApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("rules"))
{
if (handleRulesApi(req, rsp) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else if (path[2] == QLatin1String("alarmsystems"))
{
if (AS_handleAlarmSystemsApi(req, rsp, *alarmSystems, eventEmitter) == REQ_NOT_HANDLED)
{
return;
}
triggered = true;
}
else
{
DBG_Printf(DBG_INFO, "unsupported rule action address %s\n", qPrintable(ai->address()));
return;
}
if (rsp.httpStatus != HttpStatusOk)
{
DBG_Printf(DBG_INFO, "trigger rule %s - %s failed with status %s\n", qPrintable(rule.id()), qPrintable(rule.name()), rsp.httpStatus);
return;
}
}
if (triggered)
{
rule.m_lastTriggered = QDateTime::currentDateTimeUtc();
rule.setTimesTriggered(rule.timesTriggered() + 1);
rule.setNeedSaveDatabase();
updateEtag(rule.etag);
updateEtag(gwConfigEtag);
}
}
\param action - the action holding the request details
*/
int DeRestPluginPrivate::handleWebHook(const RuleAction &action)
{
QNetworkRequest req(QUrl(action.address()));
QBuffer *data = new QBuffer(webhookManager);
DBG_Assert(data);
if (!data)
{
return REQ_NOT_HANDLED;
}
data->setData(action.body().toUtf8());
QNetworkReply *reply = webhookManager->sendCustomRequest(req, action.method().toLatin1(), data);
DBG_Assert(reply);
if (reply)
{
reply->setProperty("buf", QVariant::fromValue(data));
return REQ_READY_SEND;
}
return REQ_NOT_HANDLED;
}
*/
void DeRestPluginPrivate::webhookFinishedRequest(QNetworkReply *reply)
{
if (!reply)
{
return;
}
if (reply->property("buf").canConvert<QBuffer*>())
{
QBuffer *buf = reply->property("buf").value<QBuffer*>();
buf->deleteLater();
}
DBG_Printf(DBG_INFO, "Webhook finished: %s (code: %d)\n", qPrintable(reply->url().toString()), reply->error());
if (DBG_IsEnabled(DBG_HTTP))
{
for (const auto &hdr : reply->rawHeaderPairs())
{
DBG_Printf(DBG_HTTP, "%s: %s\n", qPrintable(hdr.first), qPrintable(hdr.second));
}
QByteArray data = reply->readAll();
if (!data.isEmpty())
{
DBG_Printf(DBG_HTTP, "%s\n", qPrintable(data));
}
}
reply->deleteLater();
}
void DeRestPluginPrivate::indexRulesTriggers()
{
fastRuleCheck.clear();
for (const Rule &rule : rules)
{
if (rule.status().startsWith('e'))
{
fastRuleCheck.push_back(rule.handle());
}
}
if (!fastRuleCheckTimer->isActive() && !fastRuleCheck.empty())
{
fastRuleCheckTimer->start();
}
}
void DeRestPluginPrivate::fastRuleCheckTimerFired()
{
for (int &handle : fastRuleCheck)
{
if (handle == 0)
{
continue;
}
for (Rule &rule: rules)
{
if (rule.handle() == handle)
{
DBG_Printf(DBG_INFO_L2, "index resource items for rules, handle: %d (%s)\n", rule.handle(), qPrintable(rule.name()));
indexRuleTriggers(rule);
fastRuleCheckTimer->start();
handle = 0;
return;
}
}
handle = 0;
}
fastRuleCheck.clear();
}
void DeRestPluginPrivate::handleRuleEvent(const Event &e)
{
if (e.resource() == RDevices)
{
return;
}
Resource *resource = getResource(e.resource(), e.id());
ResourceItem *item = resource ? resource->item(e.what()) : nullptr;
const ResourceItem *localTime = config.item(RConfigLocalTime);
const QDateTime now = localTime
? QDateTime::fromMSecsSinceEpoch(localTime->toNumber())
: QDateTime::currentDateTime();
const QDateTime previousNow = (localTime && localTime->toNumberPrevious() > 0)
? QDateTime::fromMSecsSinceEpoch(localTime->toNumberPrevious())
: now.addSecs(-1);
if (!resource || !item || item->rulesInvolved().empty())
{
return;
}
if (!e.id().isEmpty())
{
DBG_Printf(DBG_INFO, "rule event %s/%s/%s: %d -> %d\n", e.resource(), qPrintable(e.id()), e.what(), e.numPrevious(), e.num());
}
else
{
DBG_Printf(DBG_INFO_L2, "rule event /%s: %s -> %s (%lldms)\n", e.what(), qPrintable(previousNow.toString("hh:mm:ss.zzz")), qPrintable(now.toString("hh:mm:ss.zzz")), previousNow.msecsTo(now));
}
std::vector<size_t> rulesToTrigger;
for (int handle : item->rulesInvolved())
{
for (size_t i = 0; i < rules.size(); i++)
{
if (rules[i].handle() != handle)
{
continue;
}
if (evaluateRule(rules[i], e, resource, item, now, previousNow))
{
rulesToTrigger.push_back(i);
}
}
}
for (size_t i : rulesToTrigger)
{
DBG_Assert(i < rules.size());
if (i < rules.size())
{
triggerRule(rules[i]);
}
}
}