Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP List JRules inside openhab web interface #90

Merged
merged 9 commits into from Mar 29, 2023
17 changes: 17 additions & 0 deletions README.md
Expand Up @@ -36,6 +36,7 @@ developer machine - like any other regular Java project - **recommended**).
+ [JRuleThings](#jrulethings)
+ [JRuleThingActions](#jrulethingactions)
* [Other built-in actions](#other-built-in-actions)
- [GUI support](#gui-support)
- [Examples](#examples)

## Why
Expand Down Expand Up @@ -140,16 +141,19 @@ equal `/etc/openhab/automation/jrule` in the following table:
### Example scripts to easily handle the copy process

#### Grab the jrule engine from the server

```shell
scp <user>@<oh-server>:$JRULE_ROOT/jar/jrule.jar lib/jrule.jar
```

#### Grab the generated jrule source

```shell
scp <user>@<oh-server>:$JRULE_ROOT/jar/jrule-generated.jar lib/jrule-generated.jar
```

#### Grab the generated jrule source

```shell
scp target/openhab-rules-1.0-SNAPSHOT.jar <user>@<oh-server>:$JRULE_ROOT/rules-jar/openhab-rules-1.0-SNAPSHOT.jar
```
Expand Down Expand Up @@ -284,6 +288,19 @@ These are method inherited from the JRule superclass.
| `postUpdate` | Post a state update to an item |
| `logXXX` | Log a Debug/Info/Warn/Error message |

## GUI support

Rules are registered in the rule registry:

![Rule registry](rules_ui.png)

Rules can be **disabled** and **re-enabled** runtime, and this will persist across restarts as this is managed by
openHAB.

![Enable/disable rule](enable_disable_rule.png)

> There is *no* support for neither writing nor executing rules directly from the GUI.

# Examples

[Examples can be found here](doc/EXAMPLES.md)
Expand Down
Binary file added enable_disable_rule.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added rules_ui.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Expand Up @@ -20,6 +20,7 @@
import org.openhab.automation.jrule.internal.engine.JRuleEngine;
import org.openhab.automation.jrule.internal.events.JRuleEventSubscriber;
import org.openhab.automation.jrule.internal.handler.JRuleHandler;
import org.openhab.automation.jrule.internal.module.JRuleRuleProvider;
import org.openhab.automation.jrule.items.JRuleItemRegistry;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.items.ItemRegistry;
Expand Down Expand Up @@ -61,16 +62,17 @@ public JRuleFactory(Map<String, Object> properties, final @Reference JRuleEventS
final @Reference ItemRegistry itemRegistry, final @Reference ThingRegistry thingRegistry,
final @Reference ThingManager thingManager, final @Reference EventPublisher eventPublisher,
final @Reference VoiceManager voiceManager, final ComponentContext componentContext,
final @Reference CronScheduler cronScheduler, final @Reference MetadataRegistry metadataRegistry) {
final @Reference CronScheduler cronScheduler, final @Reference MetadataRegistry metadataRegistry,
@Reference JRuleRuleProvider ruleProvider) {
JRuleConfig config = new JRuleConfig(properties);
config.initConfig();
jRuleEngine = JRuleEngine.get();
jRuleEngine.setConfig(config);
jRuleEngine.setItemRegistry(itemRegistry);
jRuleEngine.setCronScheduler(cronScheduler);
jRuleEngine.setRuleProvider(ruleProvider);
jRuleEngine.initialize();
JRuleItemRegistry.setMetadataRegistry(metadataRegistry);

jRuleHandler = new JRuleHandler(config, itemRegistry, thingRegistry, thingManager, eventPublisher,
eventSubscriber, voiceManager, cronScheduler, componentContext.getBundleContext(), metadataRegistry);
delayedInit.call(this::init);
Expand Down
Expand Up @@ -47,6 +47,7 @@
import org.openhab.automation.jrule.internal.JRuleConstants;
import org.openhab.automation.jrule.internal.JRuleLog;
import org.openhab.automation.jrule.internal.JRuleUtil;
import org.openhab.automation.jrule.rules.JRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -142,12 +143,12 @@ public void loadClass(ClassLoader classLoader, String className, boolean createI
if (createInstance) {
if (Modifier.isAbstract(loadedClass.getModifiers())) {
logDebug("Not creating and instance of abstract class: {}", className);
} else {
} else if (JRule.class.isAssignableFrom(loadedClass)) {
try {
final Object obj = loadedClass.getDeclaredConstructor().newInstance();
logDebug("Created instance: {} obj: {}", className, obj);
} catch (Exception x) {
logDebug("Could not create create instance using default constructor: {}", className);
logError("Could not create create instance using default constructor: {}: {}", className, x);
}
}
}
Expand Down
Expand Up @@ -56,10 +56,12 @@
import org.openhab.automation.jrule.internal.engine.timer.JRuleTimerExecutor;
import org.openhab.automation.jrule.internal.events.JRuleEventSubscriber;
import org.openhab.automation.jrule.internal.handler.JRuleTimerHandler;
import org.openhab.automation.jrule.rules.*;
import org.openhab.automation.jrule.internal.module.JRuleModuleEntry;
import org.openhab.automation.jrule.internal.module.JRuleRuleProvider;
import org.openhab.automation.jrule.rules.JRule;
import org.openhab.automation.jrule.rules.JRuleCondition;
import org.openhab.automation.jrule.rules.JRuleDebounce;
import org.openhab.automation.jrule.rules.JRuleDelayed;
import org.openhab.automation.jrule.rules.JRuleLogName;
import org.openhab.automation.jrule.rules.JRuleMemberOf;
import org.openhab.automation.jrule.rules.JRuleName;
Expand Down Expand Up @@ -106,6 +108,8 @@ public class JRuleEngine implements PropertyChangeListener {
protected JRuleLoadingStatistics ruleLoadingStatistics;
private static volatile JRuleEngine instance;

private JRuleRuleProvider ruleProvider;

public static JRuleEngine get() {
if (instance == null) {
synchronized (JRuleEngine.class) {
Expand All @@ -121,14 +125,14 @@ private JRuleEngine() {
this.ruleLoadingStatistics = new JRuleLoadingStatistics(null);
}

public void add(JRule jRule) {
logDebug("Adding rule: {}", jRule);
public void add(JRule jRule, boolean enableRule) {
logDebug("Adding rule: {}, enabled: {}", jRule, enableRule);
ruleLoadingStatistics.addRuleClass();
Arrays.stream(jRule.getClass().getDeclaredMethods()).filter(method -> !method.getName().startsWith("lambda$"))
.forEach(method -> this.add(method, jRule));
.forEach(method -> this.add(method, jRule, enableRule));
}

private void add(Method method, JRule jRule) {
private void add(Method method, JRule jRule, boolean enableRule) {
logDebug("Adding rule method: {}", method.getName());

if (!method.isAnnotationPresent(JRuleName.class)) {
Expand All @@ -153,6 +157,9 @@ private void add(Method method, JRule jRule) {
final String logName = Optional.ofNullable(method.getDeclaredAnnotation(JRuleLogName.class))
.map(JRuleLogName::value).orElse(method.getDeclaredAnnotation(JRuleName.class).value());

JRuleModuleEntry ruleModuleEntry = new JRuleModuleEntry(jRule, method,
method.getDeclaredAnnotation(JRuleName.class).value());

List<JRulePreconditionContext> jRulePreconditionContexts = Arrays
.stream(method.getAnnotationsByType(JRulePrecondition.class)).map(jRulePrecondition -> {
JRuleCondition jRuleCondition = jRulePrecondition.condition();
Expand All @@ -167,6 +174,7 @@ private void add(Method method, JRule jRule) {

final String[] loggingTags = Optional.ofNullable(method.getDeclaredAnnotation(JRuleTag.class))
.map(JRuleTag::value).orElse(EMPTY_LOG_TAGS);
ruleModuleEntry.addTags(loggingTags);

Duration timedLock = Optional.ofNullable(method.getDeclaredAnnotation(JRuleDebounce.class))
.filter(jRuleDebounce -> jRuleDebounce.value() > 0)
Expand All @@ -181,70 +189,85 @@ private void add(Method method, JRule jRule) {

Arrays.stream(method.getAnnotationsByType(JRuleWhenItemReceivedUpdate.class)).forEach(jRuleWhen -> {
JRuleCondition jRuleCondition = jRuleWhen.condition();
addToContext(new JRuleItemReceivedUpdateExecutionContext(jRule, logName, loggingTags, method,
jRuleWhen.item(), jRuleWhen.memberOf(),
JRuleItemReceivedUpdateExecutionContext context = new JRuleItemReceivedUpdateExecutionContext(jRule,
logName, loggingTags, method, jRuleWhen.item(), jRuleWhen.memberOf(),
Optional.of(new JRuleItemExecutionContext.JRuleConditionContext(jRuleCondition)),
jRulePreconditionContexts, Optional.of(jRuleWhen.state()).filter(StringUtils::isNotEmpty),
timedLock, delayed));
timedLock, delayed);
addToContext(context, enableRule);
ruleLoadingStatistics.addItemStateTrigger();
ruleModuleEntry.addJRuleWhenItemReceivedUpdate(context);
addedToContext.set(true);
});

Arrays.stream(method.getAnnotationsByType(JRuleWhenItemReceivedCommand.class)).forEach(jRuleWhen -> {
JRuleCondition jRuleCondition = jRuleWhen.condition();
addToContext(new JRuleItemReceivedCommandExecutionContext(jRule, logName, loggingTags, method,
jRuleWhen.item(), jRuleWhen.memberOf(),
JRuleItemReceivedCommandExecutionContext context = new JRuleItemReceivedCommandExecutionContext(jRule,
logName, loggingTags, method, jRuleWhen.item(), jRuleWhen.memberOf(),
Optional.of(new JRuleItemExecutionContext.JRuleConditionContext(jRuleCondition)),
jRulePreconditionContexts, Optional.of(jRuleWhen.command()).filter(StringUtils::isNotEmpty),
timedLock, delayed));
timedLock, delayed);
addToContext(context, enableRule);
ruleLoadingStatistics.addItemStateTrigger();
ruleModuleEntry.addJRuleWhenItemReceivedCommand(context);
addedToContext.set(true);
});

Arrays.stream(method.getAnnotationsByType(JRuleWhenItemChange.class)).forEach(jRuleWhen -> {
JRuleCondition jRuleCondition = jRuleWhen.condition();
JRuleCondition jRulePreviousCondition = jRuleWhen.previousCondition();
addToContext(new JRuleItemChangeExecutionContext(jRule, logName, loggingTags, method, jRuleWhen.item(),
jRuleWhen.memberOf(),
JRuleItemChangeExecutionContext context = new JRuleItemChangeExecutionContext(jRule, logName, loggingTags,
method, jRuleWhen.item(), jRuleWhen.memberOf(),
Optional.of(new JRuleItemExecutionContext.JRuleConditionContext(jRuleCondition)),
Optional.of(new JRuleItemExecutionContext.JRuleConditionContext(jRulePreviousCondition)),
jRulePreconditionContexts, Optional.of(jRuleWhen.from()).filter(StringUtils::isNotEmpty),
Optional.of(jRuleWhen.to()).filter(StringUtils::isNotEmpty), timedLock, delayed));
Optional.of(jRuleWhen.to()).filter(StringUtils::isNotEmpty), timedLock, delayed);
addToContext(context, enableRule);
ruleLoadingStatistics.addItemStateTrigger();
ruleModuleEntry.addJRuleWhenItemChange(context);
addedToContext.set(true);
});

Arrays.stream(method.getAnnotationsByType(JRuleWhenChannelTrigger.class)).forEach(jRuleWhen -> {
addToContext(new JRuleChannelExecutionContext(jRule, logName, loggingTags, method,
JRuleChannelExecutionContext context = new JRuleChannelExecutionContext(jRule, logName, loggingTags, method,
jRulePreconditionContexts, jRuleWhen.channel(),
Optional.of(jRuleWhen.event()).filter(StringUtils::isNotEmpty), timedLock, delayed));
Optional.of(jRuleWhen.event()).filter(StringUtils::isNotEmpty), timedLock, delayed);
addToContext(context, enableRule);
ruleLoadingStatistics.addChannelTrigger();
ruleModuleEntry.addJRuleWhenChannelTrigger(context);
addedToContext.set(true);
});

Arrays.stream(method.getAnnotationsByType(JRuleWhenCronTrigger.class)).forEach(jRuleWhen -> {
addToContext(new JRuleTimedCronExecutionContext(jRule, logName, loggingTags, method,
jRulePreconditionContexts, jRuleWhen.cron()));
JRuleTimedCronExecutionContext context = new JRuleTimedCronExecutionContext(jRule, logName, loggingTags,
method, jRulePreconditionContexts, jRuleWhen.cron());
addToContext(context, enableRule);
ruleLoadingStatistics.addTimedTrigger();
ruleModuleEntry.addJRuleWhenCronTrigger(context);
addedToContext.set(true);
});

Arrays.stream(method.getAnnotationsByType(JRuleWhenTimeTrigger.class)).forEach(jRuleWhen -> {
addToContext(new JRuleTimeTimerExecutionContext(jRule, logName, loggingTags, method,
jRulePreconditionContexts, Optional.of(jRuleWhen.hours()).filter(i -> i != -1),
JRuleTimeTimerExecutionContext context = new JRuleTimeTimerExecutionContext(jRule, logName, loggingTags,
method, jRulePreconditionContexts, Optional.of(jRuleWhen.hours()).filter(i -> i != -1),
Optional.of(jRuleWhen.minutes()).filter(i -> i != -1),
Optional.of(jRuleWhen.seconds()).filter(i -> i != -1)));
Optional.of(jRuleWhen.seconds()).filter(i -> i != -1));
addToContext(context, enableRule);
ruleLoadingStatistics.addTimedTrigger();
ruleModuleEntry.addJRuleWhenTimeTrigger(context);
addedToContext.set(true);
});

Arrays.stream(method.getAnnotationsByType(JRuleWhenThingTrigger.class)).forEach(jRuleWhen -> {
ruleLoadingStatistics.addThingTrigger();
addToContext(new JRuleThingExecutionContext(jRule, logName, loggingTags, method,
JRuleThingExecutionContext context = new JRuleThingExecutionContext(jRule, logName, loggingTags, method,
Optional.of(jRuleWhen.thing()).filter(StringUtils::isNotEmpty).filter(s -> !s.equals("*")),
Optional.of(jRuleWhen.from()).filter(s -> s != JRuleThingStatus.THING_UNKNOWN),
Optional.of(jRuleWhen.to()).filter(s -> s != JRuleThingStatus.THING_UNKNOWN),
jRulePreconditionContexts, timedLock, delayed));
jRulePreconditionContexts, timedLock, delayed);
addToContext(context, enableRule);
ruleLoadingStatistics.addThingTrigger();
ruleModuleEntry.addJRuleWhenThingTrigger(context);
addedToContext.set(true);
});

Expand All @@ -253,15 +276,19 @@ private void add(Method method, JRule jRule) {
logWarn("Skipping rule method {} on class {} with no JRuleWhenXXX annotation triggers", method.getName(),
jRule.getClass().getName());
}

ruleProvider.add(ruleModuleEntry);
}

private boolean addToContext(JRuleExecutionContext context) {
private boolean addToContext(JRuleExecutionContext context, boolean enableRule) {
logDebug("add to context: {}", context);
context.setEnabled(enableRule);
if (context instanceof JRuleTimedExecutionContext) {
timerExecutor.add(context);
} else {
contextList.add(context);
}

return true;
}

Expand Down Expand Up @@ -367,6 +394,7 @@ public synchronized void reset() {
timerExecutor.clear();

ruleLoadingStatistics = new JRuleLoadingStatistics(ruleLoadingStatistics);
ruleProvider.reset();
}

public boolean watchingForItem(String itemName) {
Expand Down Expand Up @@ -459,11 +487,17 @@ public Thread newThread(Runnable runnable) {
}

public void invokeRule(JRuleExecutionContext context, JRuleEvent event) {
if (config.isExecutorsEnabled()) {
ruleExecutorService.submit(() -> invokeDelayed(context, event,
(jRuleExecutionContext, jRuleEvent) -> invokeRuleInternal(context, event)));
if (context.isEnabled()) {
if (config.isExecutorsEnabled()) {
ruleExecutorService.submit(() -> invokeDelayed(context, event,
(jRuleExecutionContext, jRuleEvent) -> invokeRuleInternal(context, event)));
} else {
invokeDelayed(context, event,
(jRuleExecutionContext, jRuleEvent) -> invokeRuleInternal(context, event));
}
} else {
invokeDelayed(context, event, (jRuleExecutionContext, jRuleEvent) -> invokeRuleInternal(context, event));
JRuleLog.debug(logger, context.getLogName(), "Not invoking rule because context {} is disabled", context);

}
}

Expand Down Expand Up @@ -516,4 +550,8 @@ private void invokeDelayed(JRuleExecutionContext context, JRuleEvent event,
ruleInvoker.accept(context, event);
}
}

public void setRuleProvider(JRuleRuleProvider ruleProvider) {
this.ruleProvider = ruleProvider;
}
}
Expand Up @@ -26,6 +26,7 @@ public class JRuleLoadingStatistics {
private int numRuleClasses;

private int numRuleMethods;
private int numEnabledRules;
private final JRuleLoadingStatistics previous;

public JRuleLoadingStatistics(JRuleLoadingStatistics previous) {
Expand Down Expand Up @@ -56,6 +57,10 @@ public void addRuleMethod() {
numRuleMethods++;
}

public void addEnabledRule() {
numEnabledRules++;
}

@Override
public String toString() {
StringBuilder b = new StringBuilder();
Expand Down Expand Up @@ -89,6 +94,18 @@ public String toString() {
b.append(String.format("Loaded %d thing triggers, change %d", numThingTriggers,
previous == null ? 0 : numThingTriggers - previous.numThingTriggers));

int numDisabledRules = numRuleMethods - numEnabledRules;

if (numDisabledRules > 0) {
b.append("\n");
b.append("******** ");
if (numDisabledRules == 1) {
b.append("NOTE: 1 rule is disabled in the GUI");
} else {
b.append(String.format("NOTE: %d rules are disabled in the GUI", numDisabledRules));
}
}

return b.toString();
}
}
Expand Up @@ -35,6 +35,9 @@ public abstract class JRuleExecutionContext {
protected final Duration timedLock;
private final Duration delayed;

// If this rule is enabled or disabled in openHAB rules engine
private boolean enabled = false;

public JRuleExecutionContext(JRule rule, String logName, String[] loggingTags, Method method,
List<JRulePreconditionContext> preconditionContextList, Duration timedLock, Duration delayed) {
this.logName = logName;
Expand Down Expand Up @@ -90,6 +93,14 @@ public Duration getDelayed() {
return delayed;
}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public static class JRuleAdditionalCheckData {

}
Expand Down