/**
*
* Copyright (c) 2009-2016 Freedomotic team http://freedomotic.com
*
* This file is part of Freedomotic
*
* This Program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2, or (at your option) any later version.
*
* This Program 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
* Freedomotic; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*/
package com.freedomotic.core;
import com.freedomotic.api.EventTemplate;
import com.freedomotic.app.Freedomotic;
import com.freedomotic.bus.BusService;
import com.freedomotic.events.MessageEvent;
import com.freedomotic.exceptions.VariableResolutionException;
import com.freedomotic.behaviors.BehaviorLogic;
import com.freedomotic.exceptions.RepositoryException;
import com.freedomotic.things.EnvObjectLogic;
import com.freedomotic.things.ThingRepository;
import com.freedomotic.reactions.Command;
import com.freedomotic.reactions.Reaction;
import com.freedomotic.reactions.ReactionRepository;
import com.freedomotic.rules.Statement;
import com.freedomotic.reactions.Trigger;
import com.freedomotic.rules.Expression;
import com.freedomotic.rules.ExpressionFactory;
import com.google.inject.Inject;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Enrico Nicoletti
*/
public class TriggerCheck {
private static final Logger LOG = LoggerFactory.getLogger(TriggerCheck.class.getName());
private static final ExecutorService AUTOMATION_EXECUTOR = Executors.newCachedThreadPool();
// Dependencies
private final Autodiscovery autodiscovery;
private final BusService busService;
private final ThingRepository thingsRepository;
private final ReactionRepository reactionRepository;
private final BehaviorManager behaviorManager;
@Inject
TriggerCheck(
Autodiscovery autodiscovery,
ThingRepository thingsRepository,
BusService busService,
BehaviorManager behaviorManager,
ReactionRepository reactionRepository) {
this.autodiscovery = autodiscovery;
this.thingsRepository = thingsRepository;
this.busService = busService;
this.behaviorManager = behaviorManager;
this.reactionRepository = reactionRepository;
}
/**
* Executes trigger-event comparison in a separated thread
*
* @param event
* @param trigger
* @return
*/
public boolean check(final EventTemplate event, final Trigger trigger) {
if ((event == null) || (trigger == null)) {
throw new IllegalArgumentException("Event and Trigger cannot be null while performing trigger check");
}
try {
if (trigger.isHardwareLevel()) { //hardware triggers can always fire
Trigger resolved = resolveTrigger(event, trigger);
if (resolved.isConsistentWith(event)) {
LOG.debug("[CONSISTENT] hardware level trigger \"{} {}\"\nconsistent with received event \"{}\" \"{}\"", new Object[]{resolved.getName(), resolved.getPayload().toString(), event.getEventName(), event.getPayload().toString()});
applySensorNotification(resolved, event);
return true;
}
} else {
if (trigger.canFire()) {
Trigger resolved = resolveTrigger(event, trigger);
if (resolved.isConsistentWith(event)) {
if (LOG.isDebugEnabled()) {
LOG.debug("[CONSISTENT] registered trigger \"{} {}\"\nconsistent with received event ''{}'' {}", new Object[]{resolved.getName(), resolved.getPayload().toString(), event.getEventName(), event.getPayload().toString()});
}
executeTriggeredAutomations(resolved, event);
return true;
}
}
}
//if we are here the trigger is not consistent
if (LOG.isDebugEnabled()) {
LOG.debug("[NOT CONSISTENT] registered trigger \"{} {}\"\nnot consistent with received event ''{}'' {}", new Object[]{trigger.getName(), trigger.getPayload().toString(), event.getEventName(), event.getPayload().toString()});
}
return false;
} catch (Exception e) {
LOG.error("Error while performing trigger check", e);
return false;
}
}
/**
*
*
* @param event
* @param trigger
* @return
* @throws VariableResolutionException
*/
private Trigger resolveTrigger(final EventTemplate event, final Trigger trigger) throws VariableResolutionException {
Resolver resolver = new Resolver();
resolver.addContext("event.", event.getPayload());
return resolver.resolve(trigger);
}
/**
*
*
* @param resolved
* @param event
*/
private void applySensorNotification(Trigger resolved, final EventTemplate event) {
String protocol = null;
String address = null;
EnvObjectLogic affectedObject = null;
if (!(resolved.getPayload().getStatements("event.protocol").isEmpty() && resolved.getPayload().getStatements("event.address").isEmpty())) {
//join device: add the object on the map if not already there
//join device requires to know 'object.class' and 'object.name' properties
protocol = resolved.getPayload().getStatements("event.protocol").get(0).getValue();
address = resolved.getPayload().getStatements("event.address").get(0).getValue();
}
if ((protocol != null) && (address != null)) {
String clazz = event.getProperty("object.class");
String name = event.getProperty("object.name");
String autodiscoveryAllowClones = event.getProperty("autodiscovery.allow-clones");
affectedObject = thingsRepository.findByAddress(protocol, address);
if (affectedObject == null) { //there isn't an object with this protocol and address
LOG.warn("Found a candidate for things autodiscovery: thing \"{}\" of type \"{}\"", new Object[]{name, clazz});
if ((clazz != null) && !clazz.isEmpty()) {
boolean allowClones;
if ("false".equalsIgnoreCase(autodiscoveryAllowClones)) {
allowClones = false;
} else {
allowClones = true;
}
try {
affectedObject = autodiscovery.join(clazz, name, protocol, address, allowClones);
} catch (RepositoryException ex) {
LOG.error(Freedomotic.getStackTraceInfo(ex));
}
}
}
}
//uses trigger->behavior mapping to apply the trigger to this object
if (affectedObject != null && affectedObject.executeTrigger(resolved)) {
long elapsedTime = System.currentTimeMillis() - event.getCreation();
LOG.info(
"Sensor notification \"{}\" applied to thing \"{}\" in {} ms.",
new Object[]{resolved.getName(), affectedObject.getPojo().getName(), elapsedTime});
} else {
LOG.warn("Hardware trigger \"{}\" is not associated to any thing.", resolved.getName());
}
resolved.getPayload().clear();
event.getPayload().clear();
}
/**
*
*
* @param trigger
* @param event
*/
private void executeTriggeredAutomations(final Trigger trigger, final EventTemplate event) {
Runnable automation = new Runnable() {
@Override
public void run() {
//Searching for reactions using this trigger
boolean found = false;
for (Reaction reaction : reactionRepository.findAll()) {
Trigger reactionTrigger = reaction.getTrigger();
//found a related reaction. This must be executed
if (trigger.equals(reactionTrigger) && !reaction.getCommands().isEmpty()) {
if (!checkAdditionalConditions(reaction)) {
LOG.info("Additional conditions test failed in reaction \"{}\"", reaction.toString());
return;
}
reactionTrigger.setExecuted();
found = true;
if (LOG.isDebugEnabled()) {
LOG.debug("Try to execute reaction \"{}\"", reaction.toString());
}
try {
//executes the commands in sequence (only the first sequence is used)
//if more then one sequence is needed it can be done with two reactions with the same trigger
Resolver commandResolver = new Resolver();
event.getPayload().addStatement("description",
trigger.getDescription()); //embedd the trigger description to the event payload
commandResolver.addContext("event.",
event.getPayload());
for (final Command command : reaction.getCommands()) {
if (command == null) {
continue; //skip this loop
}
if (command.getReceiver().equalsIgnoreCase(BehaviorManager.getMessagingChannel())) {
//this command is for an object so it needs only to know only about event parameters
Command resolvedCommand = commandResolver.resolve(command);
//doing so we bypass messaging system gaining better performances
behaviorManager.parseCommand(resolvedCommand);
} else {
//if the event has a target object we include also object info
List<EnvObjectLogic> targetObjects = thingsRepository.findByName(event.getProperty("object.name"));
if (!targetObjects.isEmpty()) {
EnvObjectLogic targetObject = targetObjects.get(0);
commandResolver.addContext("current.",
targetObject.getExposedProperties());
commandResolver.addContext("current.",
targetObject.getExposedBehaviors());
}
final Command resolvedCommand = commandResolver.resolve(command);
//it's not a user level command for objects (eg: turn it on), it is for another kind of actuator
Command reply = busService.send(resolvedCommand); //blocking wait until executed
if (reply == null) {
command.setExecuted(false);
LOG.warn(
"Unreceived reply within given time ({} ms) for command \"{}\"",
new Object[]{command.getReplyTimeout(), command.getName()});
notifyMessage("Unreceived reply within given time for command \"" + command.getName() + "\"");
} else {
if (reply.isExecuted()) {
//the reply is executed so mark the origial command as executed as well
command.setExecuted(true);
LOG.debug("Executed successfully \"{}\"", command.getName());
} else {
command.setExecuted(false);
LOG.warn("Unable to execute command \"{}\". Skipping the others", command.getName());
notifyMessage("Unable to execute command \"" + command.getName() + "\"");
// skip the other commands
return;
}
}
}
}
} catch (Exception e) {
LOG.error("Exception while merging event parameters into reaction.", e);
return;
}
String info = "Executing automation \"" + reaction.toString() + "\" takes "
+ (System.currentTimeMillis() - event.getCreation()) + "ms.";
LOG.info(info);
MessageEvent message = new MessageEvent(null, info);
message.setType("callout"); //display as callout on frontends
busService.send(message);
}
}
if (!found) {
LOG.info("No valid reaction bound to trigger \"{}\"", trigger.getName());
}
trigger.getPayload().clear();
event.getPayload().clear();
}
};
AUTOMATION_EXECUTOR.execute(automation);
}
/**
* Resolves the additional conditions of the reaction in input. Now it just
* takes the statement attribute and value and check if they are equal to
* the target behavior name and value respectively. This should be improved
* to allow also REGEX and other statement resolution.
*/
private boolean checkAdditionalConditions(Reaction rea) {
boolean result = true;
for (Condition condition : rea.getConditions()) {
EnvObjectLogic object = thingsRepository.findByName(condition.getTarget()).get(0);
Statement statement = condition.getStatement();
if (object != null) {
BehaviorLogic behavior = object.getBehavior(statement.getAttribute());
String attributeValue = behavior.getValueAsString();
String operand = statement.getOperand();
String value = statement.getValue();
String valueBehavior;
//check if value is an object behavior eg. <value>[object name].temperature</value>
Pattern p = Pattern.compile("\\[(.*?)\\]\\.+[0-9A-Za-z]");
Matcher m = p.matcher(value);
if (m.find()) {
// in this case we consider the target object behavior
if (value.startsWith("[]")) {
valueBehavior = value.substring(value.indexOf('.') + 1);
behavior = object.getBehavior(valueBehavior);
if (behavior != null) {
value = behavior.getValueAsString();
}
} else {
List<EnvObjectLogic> newObject = thingsRepository.findByName(value.substring(value.indexOf('[') + 1, value.indexOf(']')));
if (newObject.isEmpty()) {
LOG.warn("Cannot test condition on unexistent thing: \"{}\"", value.substring(value.indexOf('[') + 1, value.indexOf(']')));
return false;
} else {
valueBehavior = value.substring(value.indexOf('.') + 1);
behavior = newObject.get(0).getBehavior(valueBehavior);
if (behavior != null) {
value = behavior.getValueAsString();
} else {
return false;
}
}
}
// if attributeValue and value are float and operand not "EQUALS" we must convert them to integer
if ((isDecimalNumber(attributeValue) || isDecimalNumber(value)) && !("EQUALS".equalsIgnoreCase(operand))) {
attributeValue = String.valueOf((int) Float.parseFloat(attributeValue) * 10);
value = String.valueOf((int) (Float.parseFloat(value) * 10));
}
ExpressionFactory factory = new ExpressionFactory<>();
Expression exp = factory.createExpression(attributeValue, operand, value);
boolean eval = (boolean) exp.evaluate();
if ("AND".equalsIgnoreCase(statement.getLogical())) {
result = result && eval;
} else {
result = result || eval;
}
} else {
// regex fails LOG syntax error
LOG.warn("Cannot test condition on unexistent thing: \"{}\"", condition.getTarget());
return false;
}
}
} // all is ok
return result;
}
/**
* Checks if a string represents a decimal number.
*
* @param number string to check
* @return true if it's a decimal number, false otherwise
*/
private boolean isDecimalNumber(String number) {
String decimalPattern = "([0-9]*)\\.([0-9]*)";
return Pattern.matches(decimalPattern, number);
}
/**
*
*
* @param message
*/
private void notifyMessage(String message) {
MessageEvent event = new MessageEvent(this, message);
event.setType("callout"); //display as callout on frontends
busService.send(event);
}
}