Skip to content

Commit

Permalink
[rules] Rule Builder: Add DateTime & TimeOfDay triggers & Improve typ…
Browse files Browse the repository at this point in the history
…e defs (#291)
  • Loading branch information
florian-h05 authored Sep 4, 2023
1 parent f1d9446 commit 07ad540
Show file tree
Hide file tree
Showing 16 changed files with 614 additions and 371 deletions.
32 changes: 19 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1168,34 +1168,37 @@ See [Examples](#rule-builder-examples) for further patterns.
- `.channel(channelName)` Specifies a channel event as a source for the rule to fire.
- `.triggered(event)` Trigger on a specific event name
- `.cron(cronExpression)` Specifies a cron schedule for the rule to fire.
- `.item(itemName)` Specifies an item as the source of changes to trigger a rule.
- `.timeOfDay(time)` Specifies a time of day in `HH:mm` for the rule to fire.
- `.item(itemName)` Specifies an Item as the source of changes to trigger a rule.
- `.for(duration)`
- `.from(state)`
- `.to(state)`
- `.fromOff()`
- `.toOn()`
- `.receivedCommand()`
- `.receivedUpdate()`
- `.memberOf(groupName)`
- `.memberOf(groupName)` Specifies a group Item as the source of changes to trigger the rule.
- `.for(duration)`
- `.from(state)`
- `.to(state)`
- `.fromOff()`
- `.toOn()`
- `.receivedCommand()`
- `.receivedUpdate()`
- `.system()`
- `.system()` Specifies a system event as a source for the rule to fire.
- `.ruleEngineStarted()`
- `.rulesLoaded()`
- `.startupComplete()`
- `.thingsInitialized()`
- `.userInterfacesStarted()`
- `.startLevel(level)`
- `.thing(thingName)`
- `.thing(thingName)` Specifies a Thing event as a source for the rule to fire.
- `changed()`
- `updated()`
- `from(state)`
- `to(state)`
- `.dateTime(itemName)` Specifies a DateTime Item whose (optional) date and time schedule the rule to fire.
- `.timeOnly()` Only the time of the Item should be compared, the date should be ignored.
Additionally, all the above triggers have the following functions:
Expand Down Expand Up @@ -1230,31 +1233,34 @@ Additionally, all the above triggers have the following functions:
```javascript
// Basic rule, when the BedroomLight1 is changed, run a custom function
rules.when().item('BedroomLight1').changed().then(e => {
console.log("BedroomLight1 state", e.newState)
console.log("BedroomLight1 state", e.newState)
}).build();

// Turn on the kitchen light at SUNSET
rules.when().timeOfDay("SUNSET").then().sendOn().toItem("KitchenLight").build("Sunset Rule","turn on the kitchen light at SUNSET");
// Turn on the kitchen light at SUNSET (using the Astro binding)
rules.when().channel('astro:sun:home:set#event').triggered('START').then().sendOn().toItem('KitchenLight').build('Sunset Rule', 'Turn on the kitchen light at SUNSET');

// Turn off the kitchen light at 9PM and tag rule
rules.when().cron("0 0 21 * * ?").then().sendOff().toItem("KitchenLight").build("9PM Rule", "turn off the kitchen light at 9PM", ["Tag1", "Tag2"]);
rules.when().timeOfDay('21:00').then().sendOff().toItem('KitchenLight').build('9PM Rule', 'Turn off the kitchen light at 9PM', ['Tag1', 'Tag2']);

// Set the colour of the hall light to pink at 9PM, tag rule and use a custom ID
rules.when().cron("0 0 21 * * ?").then().send("300,100,100").toItem("HallLight").build("Pink Rule", "set the colour of the hall light to pink at 9PM", ["Tag1", "Tag2"], "MyCustomID");
rules.when().cron('0 0 21 * * ?').then().send('300,100,100').toItem('HallLight').build('Pink Rule', 'Set the colour of the hall light to pink at 9PM', ['Tag1', 'Tag2'], 'MyCustomID');

// When the switch S1 status changes to ON, then turn on the HallLight
rules.when().item('S1').changed().toOn().then(sendOn().toItem('HallLight')).build("S1 Rule");
rules.when().item('S1').changed().toOn().then().sendOn().toItem('HallLight').build('S1 Rule');

// When the HallLight colour changes pink, if the function fn returns true, then toggle the state of the OutsideLight
rules.when().item('HallLight').changed().to("300,100,100").if(fn).then().sendToggle().toItem('OutsideLight').build();
rules.when().item('HallLight').changed().to('300,100,100').if(fn).then().sendToggle().toItem('OutsideLight').build();

// Turn on the outdoor lights based on a DateTime Item's time portion
rules.when().dateTime('OutdoorLights_OffTime').timeOnly().then().sendOff().toItem('OutdoorLights').build('Outdoor Lights off');

// And some rules which can be toggled by the items created in the 'gRules' Group:

// When the HallLight receives a command, send the same command to the KitchenLight
rules.when().item('HallLight').receivedCommand().then().sendIt().toItem('KitchenLight').build("Hall Light", "");
rules.when(true).item('HallLight').receivedCommand().then().sendIt().toItem('KitchenLight').build('Hall Light to Kitchen Light');

// When the HallLight is updated to ON, make sure that BedroomLight1 is set to the same state as the BedroomLight2
rules.when().item('HallLight').receivedUpdate().then().copyState().fromItem('BedroomLight1').toItem('BedroomLight2').build();
rules.when(true).item('HallLight').receivedUpdate().then().copyState().fromItem('BedroomLight1').toItem('BedroomLight2').build();
```
### Event Object
Expand Down
13 changes: 8 additions & 5 deletions rules/condition-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ConditionBuilder {
this._fn = fn;
}

/** @private */
_then (condition, fn) {
this._builder.setCondition(condition);
return new operations.OperationBuilder(this._builder, fn);
Expand All @@ -22,7 +23,7 @@ class ConditionBuilder {
/**
* Move to the rule operations
*
* @param {*} function the optional function to execute
* @param {*} fn the optional function to execute
* @returns {operations.OperationBuilder}
*/
then (fn) {
Expand Down Expand Up @@ -53,12 +54,13 @@ class ConditionBuilder {
*/
class ConditionConf {
constructor (conditionBuilder) {
/** @private */
this.conditionBuilder = conditionBuilder;
}

/**
*
* @param {*} function an optional function
* @param {*} fn an optional function
* @returns ConditionBuilder
*/
then (fn) {
Expand All @@ -81,6 +83,7 @@ class FunctionConditionConf extends ConditionConf {
*/
constructor (fn, conditionBuilder) {
super(conditionBuilder);
/** @private */
this.fn = fn;
}

Expand All @@ -92,8 +95,7 @@ class FunctionConditionConf extends ConditionConf {
* @returns {boolean} true only if the operations should be run
*/
check (...args) {
const answer = this.fn(args);
return answer;
return this.fn(args);
}
}

Expand All @@ -107,11 +109,12 @@ class FunctionConditionConf extends ConditionConf {
class ItemStateConditionConf extends ConditionConf {
constructor (itemName, conditionBuilder) {
super(conditionBuilder);
/** @private */
this.item_name = itemName;
}

/**
* Checks if item state is equal to vlaue
* Checks if item state is equal to value
* @param {*} value
* @returns {this}
*/
Expand Down
29 changes: 23 additions & 6 deletions rules/operation-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ class OperationBuilder {
this._fn = fn;
}

/** @private */
_finishErr () {
if (this._fn) {
throw new Error('rule already completed');
}
}

/** @private */
_then (operation, group, name, description, tags, id) {
this._builder.name = name;
this._builder.description = description;
Expand Down Expand Up @@ -64,9 +66,9 @@ class OperationBuilder {
* @param {string} command the command to send
* @returns {SendCommandOrUpdateOperation} the operation
*/
send (c) {
send (command) {
this._finishErr();
return new SendCommandOrUpdateOperation(this, c);
return new SendCommandOrUpdateOperation(this, command);
}

/**
Expand All @@ -75,13 +77,13 @@ class OperationBuilder {
* @param {string} update the update to send
* @returns {SendCommandOrUpdateOperation} the operation
*/
postUpdate (c) {
postUpdate (update) {
this._finishErr();
return new SendCommandOrUpdateOperation(this, c, false);
return new SendCommandOrUpdateOperation(this, update, false);
}

/**
* Specifies the a command 'ON' should be sent as a result of this rule firing.
* Specifies the command 'ON' should be sent as a result of this rule firing.
*
* @returns {SendCommandOrUpdateOperation} the operation
*/
Expand All @@ -91,7 +93,7 @@ class OperationBuilder {
}

/**
* Specifies the a command 'OFF' should be sent as a result of this rule firing.
* Specifies the command 'OFF' should be sent as a result of this rule firing.
*
* @returns {SendCommandOrUpdateOperation} the operation
*/
Expand Down Expand Up @@ -307,6 +309,7 @@ class CopyStateOperation extends OperationConfig {
class SendCommandOrUpdateOperation extends OperationConfig {
constructor (operationBuilder, dataOrSupplier, isCommand = true, optionalDesc) {
super(operationBuilder);
/** @private */
this.isCommand = isCommand;
if (typeof dataOrSupplier === 'function') {
this.dataFn = dataOrSupplier;
Expand Down Expand Up @@ -349,6 +352,7 @@ class SendCommandOrUpdateOperation extends OperationConfig {
return this;
}

/** @private */
_run (args) {
for (const toItemName of this.toItemNames) {
const item = items.getItem(toItemName);
Expand All @@ -363,10 +367,12 @@ class SendCommandOrUpdateOperation extends OperationConfig {
this.next && this.next.execute(args);
}

/** @private */
_complete () {
return (typeof this.toItemNames) !== 'undefined';
}

/** @private */
describe (compact) {
if (compact) {
return this.dataDesc + (this.isCommand ? '⌘' : '↻') + this.toItemNames + (this.next ? this.next.describe() : '');
Expand All @@ -386,6 +392,7 @@ class SendCommandOrUpdateOperation extends OperationConfig {
class ToggleOperation extends OperationConfig {
constructor (operationBuilder) {
super(operationBuilder);
/** @private */
this.next = null;
/** @type {function} */
this.toItem = function (itemName) {
Expand All @@ -397,8 +404,11 @@ class ToggleOperation extends OperationConfig {
this.next = next;
return this;
};
/** @private */
this._run = () => this.doToggle() && (this.next && this.next.execute());
/** @private */
this._complete = () => true;
/** @private */
this.describe = () => `toggle ${this.itemName}` + (this.next ? ` and ${this.next.describe()}` : '');
}

Expand Down Expand Up @@ -426,13 +436,18 @@ class TimingItemStateOperation extends OperationConfig {
throw Error('Must specify item state value to wait for!');
}

/** @private */
this.item_changed_trigger_config = itemChangedTriggerConfig;
/** @private */
this.duration_ms = (typeof duration === 'number' ? duration : parseDuration.parse(duration));

/** @private */
this._complete = itemChangedTriggerConfig._complete;
/** @private */
this.describe = () => itemChangedTriggerConfig.describe() + ' for ' + duration;
}

/** @private */
_toOHTriggers () {
// each time we're triggered, set a callback.
// If the item changes to something else, cancel the callback.
Expand All @@ -447,6 +462,7 @@ class TimingItemStateOperation extends OperationConfig {
}
}

/** @private */
_executeHook (next) {
if (items.get(this.item_changed_trigger_config.item_name).toString() === this.item_changed_trigger_config.to_value) {
this._startWait(next);
Expand All @@ -455,6 +471,7 @@ class TimingItemStateOperation extends OperationConfig {
}
}

/** @private */
_startWait (next) {
this.current_wait = setTimeout(next, this.duration_ms);
}
Expand Down
9 changes: 8 additions & 1 deletion rules/rule-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class RuleBuilder {
constructor (toggleable) {
/** @private */
this._triggerConfs = [];
/** @private */
this.toggleable = toggleable || false;
}

Expand All @@ -23,6 +24,7 @@ class RuleBuilder {
return new triggers.TriggerBuilder(this);
}

/** @private */
addTrigger (triggerConf) {
if (!triggerConf._complete()) {
throw Error('Trigger is not complete!');
Expand All @@ -31,15 +33,18 @@ class RuleBuilder {
return this;
}

/** @private */
setCondition (condition) {
if (typeof condition === 'function') {
condition = new conditions.FunctionConditionConf(condition);
}

/** @private */
this.condition = condition;
return this;
}

/** @private */
setOperation (operation, optionalRuleGroup) {
if (typeof operation === 'function') {
const operationFunction = operation;
Expand All @@ -55,7 +60,9 @@ class RuleBuilder {
}
}

/** @private */
this.operation = operation;
/** @private */
this.optionalRuleGroup = optionalRuleGroup;

const generatedTriggers = this._triggerConfs.flatMap(x => x._toOHTriggers());
Expand Down Expand Up @@ -108,7 +115,7 @@ class RuleBuilder {
module.exports = {
RuleBuilder,
/**
* Create a new {RuleBuilder} chain for easily creating rules.
* Create a new {@link RuleBuilder} chain for easily creating rules.
*
* @example <caption>Basic rule</caption>
* rules.when().item("F1_Light").changed().then().send("changed").toItem("F2_Light").build("My Rule", "My First Rule");
Expand Down
Loading

0 comments on commit 07ad540

Please sign in to comment.