/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.model.rule.runtime.internal.engine; import static org.eclipse.smarthome.model.rule.runtime.internal.engine.RuleTriggerManager.TriggerTypes.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.emf.ecore.EObject; import org.eclipse.smarthome.core.events.Event; import org.eclipse.smarthome.core.events.EventFilter; import org.eclipse.smarthome.core.events.EventSubscriber; import org.eclipse.smarthome.core.items.GenericItem; import org.eclipse.smarthome.core.items.Item; import org.eclipse.smarthome.core.items.ItemNotFoundException; import org.eclipse.smarthome.core.items.ItemRegistry; import org.eclipse.smarthome.core.items.ItemRegistryChangeListener; import org.eclipse.smarthome.core.items.StateChangeListener; import org.eclipse.smarthome.core.items.events.ItemCommandEvent; import org.eclipse.smarthome.core.items.events.ItemStateEvent; import org.eclipse.smarthome.core.thing.ThingRegistry; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.events.ChannelTriggeredEvent; import org.eclipse.smarthome.core.thing.events.ThingStatusInfoChangedEvent; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.State; import org.eclipse.smarthome.model.core.ModelRepository; import org.eclipse.smarthome.model.core.ModelRepositoryChangeListener; import org.eclipse.smarthome.model.rule.RulesStandaloneSetup; import org.eclipse.smarthome.model.rule.jvmmodel.RulesJvmModelInferrer; import org.eclipse.smarthome.model.rule.rules.Rule; import org.eclipse.smarthome.model.rule.rules.RuleModel; import org.eclipse.smarthome.model.rule.runtime.RuleEngine; import org.eclipse.smarthome.model.script.engine.Script; import org.eclipse.smarthome.model.script.engine.ScriptEngine; import org.eclipse.smarthome.model.script.engine.ScriptExecutionException; import org.eclipse.smarthome.model.script.engine.ScriptExecutionThread; import org.eclipse.xtext.naming.QualifiedName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.inject.Injector; /** * This class is the core of the Eclipse SmartHome rule engine. * It listens to changes to the rules folder, evaluates the trigger conditions of the rules and * schedules them for execution dependent on their triggering conditions. * * @author Kai Kreuzer - Initial contribution and API * @author Oliver Libutzki - Bugfixing * */ @SuppressWarnings("restriction") public class RuleEngineImpl implements ItemRegistryChangeListener, StateChangeListener, ModelRepositoryChangeListener, RuleEngine, EventSubscriber { private final Logger logger = LoggerFactory.getLogger(RuleEngineImpl.class); protected final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private ItemRegistry itemRegistry; private ModelRepository modelRepository; private ScriptEngine scriptEngine; private ThingRegistry thingRegistry; private RuleTriggerManager triggerManager; private Injector injector; private ScheduledFuture<?> startupJob; // this flag is used to signal that items are still being added and that we hence do not consider the rule engine // ready to be operational private boolean starting = true; private Runnable startupRunnable = new Runnable() { @Override public void run() { runStartupRules(); } }; public void activate() { injector = RulesStandaloneSetup.getInjector(); triggerManager = injector.getInstance(RuleTriggerManager.class); if (!isEnabled()) { logger.info("Rule engine is disabled."); return; } logger.debug("Started rule engine"); // read all rule files Iterable<String> ruleModelNames = modelRepository.getAllModelNamesOfType("rules"); ArrayList<String> clonedList = Lists.newArrayList(ruleModelNames); for (String ruleModelName : clonedList) { EObject model = modelRepository.getModel(ruleModelName); if (model instanceof RuleModel) { RuleModel ruleModel = (RuleModel) model; triggerManager.addRuleModel(ruleModel); } } // register us as listeners itemRegistry.addRegistryChangeListener(this); modelRepository.addModelRepositoryChangeListener(this); // register us on all items which are already available in the registry for (Item item : itemRegistry.getItems()) { internalItemAdded(item); } scheduleStartupRules(); } public void deactivate() { // unregister listeners for (Item item : itemRegistry.getItems()) { internalItemRemoved(item); } modelRepository.removeModelRepositoryChangeListener(this); itemRegistry.removeRegistryChangeListener(this); // execute all scripts that were registered for system shutdown executeRules(triggerManager.getRules(SHUTDOWN)); triggerManager.clearAll(); triggerManager = null; } public void setItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = itemRegistry; } public void unsetItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = null; } public void setModelRepository(ModelRepository modelRepository) { this.modelRepository = modelRepository; } public void unsetModelRepository(ModelRepository modelRepository) { this.modelRepository = null; } public void setScriptEngine(ScriptEngine scriptEngine) { this.scriptEngine = scriptEngine; } public void unsetScriptEngine(ScriptEngine scriptEngine) { this.scriptEngine = null; } public void setThingRegistry(ThingRegistry thingRegistry) { this.thingRegistry = thingRegistry; } public void unsetThingRegistry(ThingRegistry thingRegistry) { this.thingRegistry = null; } @Override public void allItemsChanged(Collection<String> oldItemNames) { // add the current items again Collection<Item> items = itemRegistry.getItems(); for (Item item : items) { internalItemAdded(item); } scheduleStartupRules(); } @Override public void added(Item item) { internalItemAdded(item); scheduleStartupRules(); } @Override public void removed(Item item) { internalItemRemoved(item); } @Override public void stateChanged(Item item, State oldState, State newState) { if (!starting && triggerManager != null) { Iterable<Rule> rules = triggerManager.getRules(CHANGE, item, oldState, newState); executeRules(rules, oldState); } } @Override public void stateUpdated(Item item, State state) { if (!starting && triggerManager != null) { Iterable<Rule> rules = triggerManager.getRules(UPDATE, item, state); executeRules(rules); } } private void receiveCommand(ItemCommandEvent commandEvent) { if (!starting && triggerManager != null && itemRegistry != null) { String itemName = commandEvent.getItemName(); Command command = commandEvent.getItemCommand(); try { Item item = itemRegistry.getItem(itemName); Iterable<Rule> rules = triggerManager.getRules(COMMAND, item, command); executeRules(rules, command); } catch (ItemNotFoundException e) { // ignore commands for non-existent items } } } private void receiveThingTrigger(ChannelTriggeredEvent event) { String triggerEvent = event.getEvent(); String channel = event.getChannel().getAsString(); Iterable<Rule> rules = triggerManager.getRules(TRIGGER, channel, triggerEvent); executeRules(rules, event); } private void receiveThingStatus(ThingStatusInfoChangedEvent event) { String thingUid = event.getThingUID().getAsString(); ThingStatus oldStatus = event.getOldStatusInfo().getStatus(); ThingStatus newStatus = event.getStatusInfo().getStatus(); Iterable<Rule> rules = triggerManager.getRules(THINGUPDATE, thingUid, newStatus); executeRules(rules); if (oldStatus != newStatus) { rules = triggerManager.getRules(THINGCHANGE, thingUid, oldStatus, newStatus); executeRules(rules, oldStatus); } } private void internalItemAdded(Item item) { if (item instanceof GenericItem) { GenericItem genericItem = (GenericItem) item; genericItem.addStateChangeListener(this); } } private void internalItemRemoved(Item item) { if (item instanceof GenericItem) { GenericItem genericItem = (GenericItem) item; genericItem.removeStateChangeListener(this); } } @Override public void modelChanged(String modelName, org.eclipse.smarthome.model.core.EventType type) { if (triggerManager != null) { if (isEnabled() && modelName.endsWith("rules")) { RuleModel model = (RuleModel) modelRepository.getModel(modelName); // remove the rules from the trigger sets if (type == org.eclipse.smarthome.model.core.EventType.REMOVED || type == org.eclipse.smarthome.model.core.EventType.MODIFIED) { triggerManager.removeRuleModel(model); } // add new and modified rules to the trigger sets if (model != null && (type == org.eclipse.smarthome.model.core.EventType.ADDED || type == org.eclipse.smarthome.model.core.EventType.MODIFIED)) { triggerManager.addRuleModel(model); // now execute all rules that are meant to trigger at startup scheduleStartupRules(); } } } } private void scheduleStartupRules() { if (startupJob != null && !startupJob.isCancelled() && !startupJob.isDone()) { startupJob.cancel(true); } startupJob = scheduler.schedule(startupRunnable, 5, TimeUnit.SECONDS); } private void runStartupRules() { if (triggerManager != null) { Iterable<Rule> startupRules = triggerManager.getRules(STARTUP); List<Rule> executedRules = Lists.newArrayList(); for (Rule rule : startupRules) { try { Script script = scriptEngine.newScriptFromXExpression(rule.getScript()); logger.debug("Executing startup rule '{}'", rule.getName()); RuleEvaluationContext context = new RuleEvaluationContext(); context.setGlobalContext(RuleContextHelper.getContext(rule, injector)); script.execute(context); executedRules.add(rule); } catch (ScriptExecutionException e) { if (!e.getMessage().contains("cannot be resolved to an item or type")) { logger.error("Error during the execution of startup rule '{}': {}", new Object[] { rule.getName(), e.getCause().getMessage() }); executedRules.add(rule); } else { logger.debug("Execution of startup rule '{}' has been postponed as items are still missing: {}", rule.getName(), e.getMessage()); } } } for (Rule rule : executedRules) { triggerManager.removeRule(STARTUP, rule); } // now that we have executed the startup rules, we are ready for others as well starting = false; triggerManager.startTimerRuleExecution(); } } protected synchronized void executeRule(Rule rule, RuleEvaluationContext context) { Script script = scriptEngine.newScriptFromXExpression(rule.getScript()); logger.debug("Executing rule '{}'", rule.getName()); context.setGlobalContext(RuleContextHelper.getContext(rule, injector)); ScriptExecutionThread thread = new ScriptExecutionThread(rule.getName(), script, context); thread.start(); } protected synchronized void executeRules(Iterable<Rule> rules) { for (Rule rule : rules) { RuleEvaluationContext context = new RuleEvaluationContext(); executeRule(rule, context); } } protected synchronized void executeRules(Iterable<Rule> rules, ChannelTriggeredEvent event) { for (Rule rule : rules) { RuleEvaluationContext context = new RuleEvaluationContext(); context.newValue(QualifiedName.create(RulesJvmModelInferrer.VAR_RECEIVED_EVENT), event); executeRule(rule, context); } } protected synchronized void executeRules(Iterable<Rule> rules, Command command) { for (Rule rule : rules) { RuleEvaluationContext context = new RuleEvaluationContext(); context.newValue(QualifiedName.create(RulesJvmModelInferrer.VAR_RECEIVED_COMMAND), command); executeRule(rule, context); } } protected synchronized void executeRules(Iterable<Rule> rules, State oldState) { for (Rule rule : rules) { RuleEvaluationContext context = new RuleEvaluationContext(); context.newValue(QualifiedName.create(RulesJvmModelInferrer.VAR_PREVIOUS_STATE), oldState); executeRule(rule, context); } } protected synchronized void executeRules(Iterable<Rule> rules, ThingStatus oldThingStatus) { for (Rule rule : rules) { RuleEvaluationContext context = new RuleEvaluationContext(); context.newValue(QualifiedName.create(RulesJvmModelInferrer.VAR_PREVIOUS_STATE), oldThingStatus.toString()); executeRule(rule, context); } } /** * we need to be able to deactivate the rule execution, otherwise the Eclipse SmartHome designer would also execute * the rules. * * @return true, if rules should be executed, false otherwise */ private boolean isEnabled() { return !"true".equalsIgnoreCase(System.getProperty("noRules")); } @Override public void updated(Item oldItem, Item item) { removed(oldItem); added(item); } private final Set<String> subscribedEventTypes = ImmutableSet.of(ItemStateEvent.TYPE, ItemCommandEvent.TYPE, ChannelTriggeredEvent.TYPE, ThingStatusInfoChangedEvent.TYPE); @Override public Set<String> getSubscribedEventTypes() { return subscribedEventTypes; } @Override public EventFilter getEventFilter() { return null; } @Override public void receive(Event event) { if (event instanceof ItemCommandEvent) { receiveCommand((ItemCommandEvent) event); } else if (event instanceof ChannelTriggeredEvent) { receiveThingTrigger((ChannelTriggeredEvent) event); } else if (event instanceof ThingStatusInfoChangedEvent) { receiveThingStatus((ThingStatusInfoChangedEvent) event); } } }