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

[rules] Rule Builder: Add DateTime & TimeOfDay triggers & Improve type defs #291

Merged
merged 10 commits into from
Sep 4, 2023
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
Loading