iobroker.javascript
Version:
Rules Engine for ioBroker
1,418 lines (1,224 loc) • 62.9 kB
JavaScript
'use strict';
if (typeof goog !== 'undefined') {
goog.provide('Blockly.JavaScript.Trigger');
goog.require('Blockly.JavaScript');
}
Blockly.CustomBlocks = Blockly.CustomBlocks || [];
Blockly.CustomBlocks.push('Trigger');
Blockly.Trigger = {
HUE: 330,
blocks: {},
WARNING_PARENTS: [
'on', 'on_ext', 'schedule', 'schedule_by_id', 'schedule_create', 'astro', 'onMessage', 'onFile', 'onLog', 'onEnumMembers', // trigger blocks
'timeouts_setinterval', 'timeouts_setinterval_variable', // timeouts
'controls_repeat_ext', 'controls_repeat_ext', 'controls_for', 'controls_forEach', // loops
],
};
// --- ON Extended-----------------------------------------------------------
Blockly.Trigger.blocks['on_ext'] =
'<block type="on_ext">' +
' <mutation items="1"></mutation>' +
' <field name="CONDITION">ne</field>' +
' <field name="ACK_CONDITION"></field>' +
' <value name="OID0">' +
' <shadow type="field_oid">' +
' </shadow>' +
' </value>' +
'</block>';
Blockly.Blocks['on_ext_oid_container'] = {
/**
* Mutator block for container.
* @this Blockly.Block
*/
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('on_ext_on'));
this.appendStatementInput('STACK');
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('on_ext_on_tooltip'));
this.contextMenu = false;
},
};
Blockly.Blocks['on_ext_oid'] = {
/**
* Mutator block for add items.
* @this Blockly.Block
*/
init: function () {
this.appendDummyInput('OID')
.appendField(Blockly.Translate('on_ext_oid'));
this.setPreviousStatement(true);
this.setNextStatement(true);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('on_ext_oid_tooltip'));
this.contextMenu = false;
},
};
Blockly.Blocks['on_ext'] = {
init: function () {
this.itemCount_ = 1;
this.setMutator(new Blockly.icons.MutatorIcon(['on_ext_oid'], this));
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('on_ext_tooltip'));
this.setHelpUrl(getHelp('on_help'));
},
/**
* Create XML to represent number of text inputs.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function () {
const container = document.createElement('mutation');
container.setAttribute('items', this.itemCount_);
return container;
},
/**
* Parse XML to restore the text inputs.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function (xmlElement) {
this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
this.updateShape_();
},
/**
* Populate the mutator's dialog with this block's components.
* @param {!Blockly.Workspace} workspace Mutator's workspace.
* @return {!Blockly.Block} Root block in mutator.
* @this Blockly.Block
*/
decompose: function (workspace) {
const containerBlock = workspace.newBlock('on_ext_oid_container');
containerBlock.initSvg();
let connection = containerBlock.getInput('STACK').connection;
for (let i = 0; i < this.itemCount_; i++) {
const itemBlock = workspace.newBlock('on_ext_oid');
itemBlock.initSvg();
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},
/**
* Reconfigure this block based on the mutator dialog's components.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
compose: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock('STACK');
// Count number of inputs.
const connections = [];
while (itemBlock) {
connections.push(itemBlock.valueConnection_);
itemBlock = itemBlock.nextConnection &&
itemBlock.nextConnection.targetBlock();
}
// Disconnect any children that don't belong.
for (let k = 0; k < this.itemCount_; k++) {
try {
const connection = this.getInput('OID' + k).connection.targetConnection;
if (connection && !connections.includes(connection)) {
connection.disconnect();
}
} catch (e) {
// Do nothing, input does not exist
}
}
this.itemCount_ = connections.length;
if (this.itemCount_ < 1) {
this.itemCount_ = 1;
}
this.updateShape_();
// Reconnect any child blocks.
for (let i = 0; i < this.itemCount_; i++) {
Blockly.icons.MutatorIcon.reconnect(connections[i], this, 'OID' + i);
}
},
/**
* Store pointers to any connected child blocks.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this Blockly.Block
*/
saveConnections: function (containerBlock) {
let itemBlock = containerBlock.getInputTargetBlock('STACK');
let i = 0;
while (itemBlock) {
let input;
try {
input = this.getInput('OID' + i);
} catch (e) {
input = null;
}
itemBlock.valueConnection_ = input?.connection.targetConnection;
i++;
itemBlock = itemBlock.nextConnection?.targetBlock();
}
},
/**
* Modify this block to have the correct number of inputs.
* @private
* @this Blockly.Block
*/
updateShape_: function () {
let conditionValue = undefined;
try {
if (this.getInput('CONDITION')) {
conditionValue = this.getFieldValue('CONDITION');
this.removeInput('CONDITION');
}
} catch (e) {
// Do nothing, input does not exist
}
let conditionAckValue = undefined;
try {
if (this.getInput('ACK_CONDITION')) {
conditionAckValue = this.getFieldValue('ACK_CONDITION');
this.removeInput('ACK_CONDITION');
}
} catch (e) {
// Do nothing, input does not exist
}
let input;
for (let j = 0; input = this.inputList[j]; j++) {
if (input.name === 'STATEMENT') {
this.inputList.splice(j, 1);
break;
}
}
// Add new inputs.
const wp = this.workspace;
let i;
for (i = 0; i < this.itemCount_; i++) {
let _input;
try {
_input = this.getInput('OID' + i);
} catch (e) {
// Do nothing, input does not exist
}
if (!_input) {
_input = this.appendValueInput('OID' + i);
if (i === 0) {
_input.appendField(Blockly.Translate('on_ext'));
}
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const shadow = wp.newBlock('field_oid');
shadow.setShadow(true);
shadow.outputConnection.connect(__input.connection);
shadow.initSvg();
shadow.render();
}
}, 100, _input);
} else {
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const shadow = wp.newBlock('field_oid');
shadow.setShadow(true);
shadow.outputConnection.connect(__input.connection);
shadow.initSvg();
shadow.render();
}
}, 100, _input);
}
}
// Remove deleted inputs.
try {
while (this.getInput('OID' + i)) {
this.removeInput('OID' + i);
i++;
}
} catch (e) {
// Do nothing, input does not exist
}
this.appendDummyInput('CONDITION')
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('on_onchange'), 'ne'],
[Blockly.Translate('on_any'), 'any'],
[Blockly.Translate('on_gt'), 'gt'],
[Blockly.Translate('on_ge'), 'ge'],
[Blockly.Translate('on_lt'), 'lt'],
[Blockly.Translate('on_le'), 'le'],
[Blockly.Translate('on_true'), 'true'],
[Blockly.Translate('on_false'), 'false'],
]), 'CONDITION');
if (conditionValue) {
this.setFieldValue(conditionValue, 'CONDITION'); // restore previous value
}
this.appendDummyInput('ACK_CONDITION')
.appendField(Blockly.Translate('on_ack'))
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('on_ack_any'), ''],
[Blockly.Translate('on_ack_true'), 'true'],
[Blockly.Translate('on_ack_false'), 'false'],
]), 'ACK_CONDITION');
if (conditionAckValue) {
this.setFieldValue(conditionAckValue, 'ACK_CONDITION'); // restore previous value
}
if (input) {
this.inputList.push(input);
} else {
this.appendStatementInput('STATEMENT')
.setCheck(null);
}
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = true;
// Is the block nested in a trigger?
let block = this;
while (block = block.getSurroundParent()) {
if (block && Blockly.Trigger.WARNING_PARENTS.includes(block.type)) {
legal = false;
break;
}
}
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('trigger_in_trigger_warning'), this.id);
}
},
};
Blockly.JavaScript.forBlock['on_ext'] = function (block) {
const fCondition = block.getFieldValue('CONDITION');
const fAckCondition = block.getFieldValue('ACK_CONDITION');
let val;
if (fCondition === 'true' || fCondition === 'false') {
val = `val: ${fCondition}`;
} else {
val = `change: '${fCondition}'`;
}
const oids = [];
for (let n = 0; n < block.itemCount_; n++) {
let id = Blockly.JavaScript.valueToCode(block, 'OID' + n, Blockly.JavaScript.ORDER_COMMA);
if (id) {
id = id.toString();
if (id.startsWith('\'') && id.endsWith('\'')) {
id = `[${id}]`;
}
if (oids.indexOf(id) === -1) {
oids.push(id);
}
}
}
const oid = `[].concat(${oids.join(').concat(')})`;
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
return `on({ id: ${oid}, ${val}${fAckCondition ? `, ack: ${fAckCondition}` : ''} }, async (obj) => {\n` +
(oids.length === 1 ? Blockly.JavaScript.prefixLines('let value = obj.state.val;\nlet oldValue = obj.oldState.val;', Blockly.JavaScript.INDENT) + '\n' : '') +
statement +
'});\n';
};
// --- ON -----------------------------------------------------------
Blockly.Trigger.blocks['on'] =
'<sep gap="5"></sep>' +
'<block type="on">' +
' <field name="CONDITION">ne</field>' +
' <field name="ACK_CONDITION"></field>' +
'</block>';
Blockly.Blocks['on'] = {
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('on'));
this.appendDummyInput('OID')
.appendField(new Blockly.FieldOID(Blockly.Translate('select_id'), 'state'), 'OID');
this.appendDummyInput('CONDITION')
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('on_onchange'), 'ne'],
[Blockly.Translate('on_any'), 'any'],
[Blockly.Translate('on_gt'), 'gt'],
[Blockly.Translate('on_ge'), 'ge'],
[Blockly.Translate('on_lt'), 'lt'],
[Blockly.Translate('on_le'), 'le'],
[Blockly.Translate('on_true'), 'true'],
[Blockly.Translate('on_false'), 'false'],
]), 'CONDITION');
this.appendDummyInput('ACK_CONDITION')
.appendField(Blockly.Translate('on_ack'))
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('on_ack_any'), ''],
[Blockly.Translate('on_ack_true'), 'true'],
[Blockly.Translate('on_ack_false'), 'false'],
]), 'ACK_CONDITION');
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('on_tooltip'));
this.setHelpUrl(getHelp('on_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = true;
// Is the block nested in a trigger?
let block = this;
while (block = block.getSurroundParent()) {
if (block && Blockly.Trigger.WARNING_PARENTS.includes(block.type)) {
legal = false;
break;
}
}
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('trigger_in_trigger_warning'), this.id);
}
},
};
Blockly.JavaScript.forBlock['on'] = function (block) {
const fObjId = block.getFieldValue('OID');
const fCondition = block.getFieldValue('CONDITION');
const fAckCondition = block.getFieldValue('ACK_CONDITION');
Blockly.Msg.VARIABLES_DEFAULT_NAME = 'value';
let val;
if (fCondition === 'true' || fCondition === 'false') {
val = `val: ${fCondition}`;
} else {
val = `change: '${fCondition}'`;
}
let objectName = main.objects[fObjId] && main.objects[fObjId].common && main.objects[fObjId].common.name ? main.objects[fObjId].common.name : '';
if (typeof objectName === 'object') {
objectName = objectName[systemLang] || objectName.en;
}
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
return `on({ id: '${fObjId}'${objectName ? ` /* ${objectName} */` : ''}, ${val}${fAckCondition ? `, ack: ${fAckCondition}` : ''} }, async (obj) => {\n` +
Blockly.JavaScript.prefixLines('let value = obj.state.val;', Blockly.JavaScript.INDENT) + '\n' +
Blockly.JavaScript.prefixLines('let oldValue = obj.oldState.val;', Blockly.JavaScript.INDENT) + '\n' +
statement +
'});\n';
};
// --- get info about event -----------------------------------------------------------
Blockly.Trigger.blocks['on_source'] =
'<sep gap="5"></sep>' +
'<block type="on_source">' +
' <field name="ATTR">state.val</field>' +
'</block>';
Blockly.Blocks['on_source'] = {
/**
* Block for conditionally returning a value from a procedure.
* @this Blockly.Block
*/
init: function () {
this.appendDummyInput()
.appendField('↪');
this.appendDummyInput('ATTR')
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('on_source_state_val'), 'state.val'],
[Blockly.Translate('on_source_state_ts'), 'state.ts'],
[Blockly.Translate('on_source_state_q'), 'state.q'],
[Blockly.Translate('on_source_state_from'), 'state.from'],
[Blockly.Translate('on_source_state_ack'), 'state.ack'],
[Blockly.Translate('on_source_state_lc'), 'state.lc'],
[Blockly.Translate('on_source_state_c'), 'state.c'],
[Blockly.Translate('on_source_state_user'), 'state.user'],
[Blockly.Translate('on_source_id'), 'id'],
[Blockly.Translate('on_source_name'), 'common.name'],
[Blockly.Translate('on_source_desc'), 'common.desc'],
[Blockly.Translate('on_source_channel_id'), 'channelId'],
[Blockly.Translate('on_source_channel_name'), 'channelName'],
[Blockly.Translate('on_source_device_id'), 'deviceId'],
[Blockly.Translate('on_source_device_name'), 'deviceName'],
[Blockly.Translate('on_source_oldstate_val'), 'oldState.val'],
[Blockly.Translate('on_source_oldstate_ts'), 'oldState.ts'],
[Blockly.Translate('on_source_oldstate_q'), 'oldState.q'],
[Blockly.Translate('on_source_oldstate_from'), 'oldState.from'],
[Blockly.Translate('on_source_oldstate_ack'), 'oldState.ack'],
[Blockly.Translate('on_source_oldstate_lc'), 'oldState.lc'],
[Blockly.Translate('on_source_oldstate_c'), 'oldState.c'],
[Blockly.Translate('on_source_oldstate_user'), 'oldState.user'],
]), 'ATTR');
this.setInputsInline(true);
this.setOutput(true);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('on_source_tooltip'));
this.setHelpUrl(getHelp('on_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = false;
// Is the block nested in a trigger?
let block = this;
do {
if (this.FUNCTION_TYPES.includes(block.type)) {
legal = true;
break;
}
block = block.getSurroundParent();
} while (block);
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('on_source_warning'), this.id);
}
},
/**
* List of block types that are functions and thus do not need warnings.
* To add a new function type add this to your code:
* Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func');
*/
FUNCTION_TYPES: ['on', 'on_ext', 'onEnumMembers'],
};
Blockly.JavaScript.forBlock['on_source'] = function (block) {
let fAttr = block.getFieldValue('ATTR');
const parts = fAttr.split('.');
if (parts.length > 1) {
fAttr = `(obj.${parts[0]} ? obj.${fAttr} : '')`;
} else {
fAttr = `obj.${fAttr}`;
}
return [fAttr, Blockly.JavaScript.ORDER_ATOMIC];
};
// --- acknowledge -----------------------------------------------------------
Blockly.Trigger.blocks['on_ack_value'] =
'<sep gap="5"></sep>' +
'<block type="on_ack_value">' +
'</block>';
Blockly.Blocks['on_ack_value'] = {
/**
* Block for conditionally returning a value from a procedure.
* @this Blockly.Block
*/
init: function () {
this.appendDummyInput()
.appendField('↪ ' + Blockly.Translate('on_ack_value'));
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('on_ack_value_tooltip'));
this.setHelpUrl(getHelp('on_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = false;
// Is the block nested in a trigger?
let block = this;
do {
if (this.FUNCTION_TYPES.includes(block.type)) {
legal = true;
break;
}
block = block.getSurroundParent();
} while (block);
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('on_ack_value_warning'), this.id);
}
},
/**
* List of block types that are functions and thus do not need warnings.
* To add a new function type add this to your code:
* Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func');
*/
FUNCTION_TYPES: ['on', 'on_ext', 'onEnumMembers'],
};
Blockly.JavaScript.forBlock['on_ack_value'] = function (block) {
return 'if (obj.id && obj?.state && !obj.state.ack) {\n' +
Blockly.JavaScript.prefixLines(`await setStateAsync(obj.id, { val: obj.state.val, ack: true });`, Blockly.JavaScript.INDENT) + '\n' +
`}\n`;
};
// --- ASTRO -----------------------------------------------------------
Blockly.Trigger.blocks['astro'] =
'<block type="astro">' +
' <field name="TYPE">sunrise</field>' +
' <field name="OFFSET">0</field>' +
'</block>';
Blockly.Blocks['astro'] = {
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('astro'));
this.appendDummyInput('TYPE')
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('astro_sunriseText'), 'sunrise'],
[Blockly.Translate('astro_sunriseEndText'), 'sunriseEnd'],
[Blockly.Translate('astro_goldenHourEndText'), 'goldenHourEnd'],
[Blockly.Translate('astro_solarNoonText'), 'solarNoon'],
[Blockly.Translate('astro_goldenHourText'), 'goldenHour'],
[Blockly.Translate('astro_sunsetStartText'), 'sunsetStart'],
[Blockly.Translate('astro_sunsetText'), 'sunset'],
[Blockly.Translate('astro_duskText'), 'dusk'],
[Blockly.Translate('astro_nauticalDuskText'), 'nauticalDusk'],
[Blockly.Translate('astro_nightText'), 'night'],
[Blockly.Translate('astro_nightEndText'), 'nightEnd'],
[Blockly.Translate('astro_nauticalDawnText'), 'nauticalDawn'],
[Blockly.Translate('astro_dawnText'), 'dawn'],
[Blockly.Translate('astro_nadirText'), 'nadir'],
]), 'TYPE');
this.appendDummyInput()
.appendField(Blockly.Translate('astro_offset'));
this.appendDummyInput('OFFSET')
.appendField(new Blockly.FieldTextInput('0'), 'OFFSET');
this.appendDummyInput()
.appendField(Blockly.Translate('astro_minutes'));
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('astro_tooltip'));
this.setHelpUrl(getHelp('astro_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = true;
// Is the block nested in a trigger?
let block = this;
while (block = block.getSurroundParent()) {
if (block && Blockly.Trigger.WARNING_PARENTS.includes(block.type)) {
legal = false;
break;
}
}
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('trigger_in_trigger_warning'), this.id);
}
},
};
Blockly.JavaScript.forBlock['astro'] = function (block) {
const fType = block.getFieldValue('TYPE');
const fOffset = parseInt(block.getFieldValue('OFFSET'), 10);
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
return `schedule({ astro: '${fType}', shift: ${fOffset} }, async () => {\n` +
statement +
'});\n';
};
// --- SCHEDULE -----------------------------------------------------------
Blockly.Trigger.blocks['schedule'] =
'<block type="schedule">' +
' <field name="SCHEDULE">* * * * *</field>' +
'</block>';
Blockly.Blocks['schedule'] = {
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('schedule'));
this.appendDummyInput('SCHEDULE')
.appendField(new Blockly.FieldCRON('* * * * *'), 'SCHEDULE');
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('schedule_tooltip'));
this.setHelpUrl(getHelp('schedule_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = true;
// Is the block nested in a trigger?
let block = this;
while (block = block.getSurroundParent()) {
if (block && Blockly.Trigger.WARNING_PARENTS.includes(block.type)) {
legal = false;
break;
}
}
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('trigger_in_trigger_warning'), this.id);
}
},
};
Blockly.JavaScript.forBlock['schedule'] = function (block) {
let fSchedule = block.getFieldValue('SCHEDULE');
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
if (fSchedule.startsWith('{')) {
fSchedule = `'${fSchedule}'`;
} else {
fSchedule = `"${fSchedule}"`;
}
return `schedule(${fSchedule}, async () => {\n` +
statement +
'});\n';
};
// --- SCHEDULE BY ID -----------------------------------------------------
Blockly.Trigger.blocks['schedule_by_id'] =
'<block type="schedule_by_id">' +
' <field name="ACK_CONDITION"></field>' +
'</block>';
Blockly.Blocks['schedule_by_id'] = {
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('schedule_by_id'));
this.appendDummyInput('OID')
.appendField(new Blockly.FieldOID(Blockly.Translate('select_id'), 'state'), 'OID');
this.appendDummyInput('ACK_CONDITION')
.appendField(Blockly.Translate('on_ack'))
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('on_ack_any'), ''],
[Blockly.Translate('on_ack_true'), 'true'],
[Blockly.Translate('on_ack_false'), 'false'],
]), 'ACK_CONDITION');
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('schedule_by_id_tooltip'));
this.setHelpUrl(getHelp('schedule_by_id_help'));
},
};
Blockly.JavaScript.forBlock['schedule_by_id'] = function (block) {
const fObjId = block.getFieldValue('OID');
const fAckCondition = block.getFieldValue('ACK_CONDITION');
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
let objectName = main.objects[fObjId] && main.objects[fObjId].common && main.objects[fObjId].common.name ? main.objects[fObjId].common.name : '';
if (typeof objectName === 'object') {
objectName = objectName[systemLang] || objectName.en;
}
return `scheduleById('${fObjId}'${objectName ? ` /* ${objectName} */` : ''}${fAckCondition ? `, ${fAckCondition}` : ''}, async () => {\n` +
statement +
'});\n';
};
// --- set named schedule -----------------------------------------------------------
Blockly.Trigger.blocks['schedule_create'] =
'<block type="schedule_create">' +
' <field name="NAME">schedule</field>' +
' <value name="SCHEDULE">' +
' <shadow type="field_cron">' +
' </shadow>' +
' </value>' +
'</block>';
/**
* Ensure two identically-named procedures don't exist.
* @param {string} name Proposed procedure name.
* @param {!Blockly.Block} block Block to disambiguate.
* @return {string} Non-colliding name.
*/
Blockly.Trigger.findLegalName = function (name, block) {
if (block.isInFlyout) {
// Flyouts can have multiple procedures called 'do something'.
return name;
}
while (!Blockly.Trigger.isLegalName_(name, block.workspace, block)) {
// Collision with another procedure.
const r = name.match(/^(.*?)(\d+)$/);
if (!r) {
name += '1';
} else {
name = r[1] + (parseInt(r[2], 10) + 1);
}
}
return name;
};
/**
* Does this procedure have a legal name? Illegal names include names of
* procedures already defined.
* @param {string} name The questionable name.
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
* comparisons (one doesn't want to collide with oneself).
* @return {boolean} True if the name is legal.
* @private
*/
Blockly.Trigger.isLegalName_ = function (name, workspace, opt_exclude) {
if (name === 'schedule') {
return false;
}
const blocks = workspace.getAllBlocks();
// Iterate through every block and check the name.
for (let i = 0; i < blocks.length; i++) {
if (blocks[i] == opt_exclude) {
continue;
}
if (blocks[i].isSchedule_) {
const blockName = blocks[i].getFieldValue('NAME');
if (Blockly.Names.equals(blockName, name)) {
return false;
}
}
}
return true;
};
/**
* Rename a procedure. Called by the editable field.
* @param {string} name The proposed new name.
* @return {string} The accepted name.
* @this {!Blockly.Field}
*/
Blockly.Trigger.rename = function (name) {
// Strip leading and trailing whitespace. Beyond this, all names are legal.
name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
return Blockly.Trigger.findLegalName(name, this.sourceBlock_);
};
Blockly.Blocks['schedule_create'] = {
init: function () {
const nameField = new Blockly.FieldTextInput(
Blockly.Trigger.findLegalName('schedule', this),
Blockly.Trigger.rename);
nameField.setSpellcheck(false);
this.appendDummyInput('NAME')
.appendField(Blockly.Translate('schedule_create'))
.appendField(nameField, 'NAME');
this.appendValueInput('SCHEDULE')
.appendField(Blockly.Translate('schedule_text'));
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('schedule_create_tooltip'));
this.setHelpUrl(getHelp('schedule_create_help'));
},
isSchedule_: true,
getVars: function () {
return [this.getFieldValue('NAME')];
},
getVarModels: function () {
const name = this.getFieldValue('NAME');
return [{ getId: () => name, name: name, type: 'cron' }];
},
};
Blockly.JavaScript.forBlock['schedule_create'] = function (block) {
const fName = Blockly.JavaScript.nameDB_.safeName(block.getFieldValue('NAME'));
const vSchedule = Blockly.JavaScript.valueToCode(block, 'SCHEDULE', Blockly.JavaScript.ORDER_ATOMIC);
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
return `${fName} = schedule(${vSchedule}, async () => {\n` +
statement +
'});\n';
};
// --- clearSchedule -----------------------------------------------------------
Blockly.Trigger.getAllSchedules = function (workspace) {
const blocks = workspace.getAllBlocks();
const result = [];
// Iterate through every block and check the name.
for (let i = 0; i < blocks.length; i++) {
if (blocks[i].isSchedule_) {
result.push([blocks[i].getFieldValue('NAME'), blocks[i].getFieldValue('NAME')]);
}
}
// BF(2020.05.16): for back compatibility. Remove it after 5 years
if (window.scripts.loading) {
const variables = workspace.getVariablesOfType('');
variables.forEach(v => !result.find(it => it[0] === v.name) && result.push([v.name, v.name]));
}
const variables1 = workspace.getVariablesOfType('cron');
variables1.forEach(v => !result.find(it => it[0] === v.name) && result.push([v.name, v.name]));
!result.length && result.push(['', '']);
return result;
};
Blockly.Trigger.blocks['schedule_clear'] =
'<sep gap="5"></sep>' +
'<block type="schedule_clear">' +
' <field name="NAME"></field>' +
'</block>';
Blockly.Blocks['schedule_clear'] = {
init: function () {
this.appendDummyInput('NAME')
.appendField(Blockly.Translate('schedule_clear'))
.appendField(new Blockly.FieldDropdown(function () {
return scripts.blocklyWorkspace ? Blockly.Trigger.getAllSchedules(scripts.blocklyWorkspace) : [];
}), 'NAME');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('schedule_clear_tooltip'));
this.setHelpUrl(getHelp('schedule_clear_help'));
},
};
Blockly.JavaScript.forBlock['schedule_clear'] = function (block) {
const fName = Blockly.JavaScript.nameDB_.safeName(block.getFieldValue('NAME'));
return `(() => { if (${fName}) { clearSchedule(${fName}); ${fName} = null; }})();\n`;
};
// --- CRON dialog --------------------------------------------------
Blockly.Trigger.blocks['field_cron'] =
'<sep gap="5"></sep>' +
'<block type="field_cron">' +
' <field name="CRON">* * * * *</field>' +
'</block>';
Blockly.Blocks['field_cron'] = {
// Checkbox.
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('field_cron_CRON'));
this.appendDummyInput()
.appendField(new Blockly.FieldCRON('* * * * *'), 'CRON');
this.setInputsInline(true);
this.setOutput(true, 'String');
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('field_cron_tooltip'));
},
};
Blockly.JavaScript.forBlock['field_cron'] = function (block) {
const fCron = block.getFieldValue('CRON');
return [`'${fCron}'`, Blockly.JavaScript.ORDER_ATOMIC];
};
// --- CRON builder --------------------------------------------------
Blockly.Trigger.blocks['cron_builder'] =
'<sep gap="5"></sep>' +
'<block type="cron_builder">' +
' <mutation seconds="false" as_line="false"></mutation>' +
' <field name="LINE">FALSE</field>' +
' <field name="WITH_SECONDS">FALSE</field>' +
' <value name="DOW">' +
' <shadow type="text">' +
' <field name="TEXT">*</field>' +
' </shadow>' +
' </value>' +
' <value name="MONTHS">' +
' <shadow type="text">' +
' <field name="TEXT">*</field>' +
' </shadow>' +
' </value>' +
' <value name="DAYS">' +
' <shadow type="text">' +
' <field name="TEXT">*</field>' +
' </shadow>' +
' </value>' +
' <value name="HOURS">' +
' <shadow type="text">' +
' <field name="TEXT">*</field>' +
' </shadow>' +
' </value>' +
' <value name="MINUTES">' +
' <shadow type="text">' +
' <field name="TEXT">*</field>' +
' </shadow>' +
' </value>' +
'</block>';
Blockly.Blocks['cron_builder'] = {
// Checkbox.
init: function () {
this.appendDummyInput()
.appendField(Blockly.Translate('cron_builder_CRON'));
this.appendDummyInput('LINE')
.appendField(Blockly.Translate('cron_builder_line'))
.appendField(new Blockly.FieldCheckbox('FALSE', function (option) {
this.sourceBlock_.setInputsInline(option === true || option === 'true' || option === 'TRUE');
}), 'LINE');
let _input = this.appendValueInput('DOW')
.appendField(Blockly.Translate('cron_builder_dow'));
const wp = this.workspace;
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const _shadow = wp.newBlock('text');
_shadow.setShadow(true);
_shadow.setFieldValue('*', 'TEXT');
_shadow.outputConnection.connect(__input.connection);
}
}, 100, _input);
_input = this.appendValueInput('MONTHS')
.appendField(Blockly.Translate('cron_builder_month'));
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const _shadow = wp.newBlock('text');
_shadow.setShadow(true);
_shadow.setFieldValue('*', 'TEXT');
_shadow.outputConnection.connect(__input.connection);
}
}, 100, _input);
_input = this.appendValueInput('DAYS')
.appendField(Blockly.Translate('cron_builder_day'));
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const _shadow = wp.newBlock('text');
_shadow.setShadow(true);
_shadow.setFieldValue('*', 'TEXT');
_shadow.outputConnection.connect(__input.connection);
}
}, 100, _input);
_input = this.appendValueInput('HOURS')
.appendField(Blockly.Translate('cron_builder_hour'));
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const _shadow = wp.newBlock('text');
_shadow.setShadow(true);
_shadow.setFieldValue('*', 'TEXT');
_shadow.outputConnection.connect(__input.connection);
}
}, 100, _input);
_input = this.appendValueInput('MINUTES')
.appendField(Blockly.Translate('cron_builder_minutes'));
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const _shadow = wp.newBlock('text');
_shadow.setShadow(true);
_shadow.setFieldValue('*', 'TEXT');
_shadow.outputConnection.connect(__input.connection);
}
}, 100, _input);
this.appendDummyInput('WITH_SECONDS')
.appendField(Blockly.Translate('cron_builder_with_seconds'))
.appendField(new Blockly.FieldCheckbox('FALSE', function (option) {
const withSeconds = option === true || option === 'true' || option === 'TRUE';
this.sourceBlock_.updateShape_(withSeconds);
}), 'WITH_SECONDS');
this.seconds_ = false;
this.as_line_ = false;
this.setInputsInline(this.as_line_);
this.setOutput(true, 'String');
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('field_cron_tooltip'));
},
/**
* Create XML to represent the number of text inputs.
* @return {!Element} XML storage element.
* @this Blockly.Block
*/
mutationToDom: function () {
const container = document.createElement('mutation');
container.setAttribute('seconds', this.seconds_);
container.setAttribute('as_line', this.as_line_);
return container;
},
/**
* Parse XML to restore the text inputs.
* @param {!Element} xmlElement XML storage element.
* @this Blockly.Block
*/
domToMutation: function (xmlElement) {
this.seconds_ = xmlElement.getAttribute('seconds') === 'true';
this.as_line_ = xmlElement.getAttribute('as_line') === 'true';
this.setInputsInline(this.as_line_);
this.updateShape_(this.seconds_);
},
updateShape_: function (withSeconds) {
this.seconds_ = withSeconds;
// Add or remove a statement Input.
let inputExists;
try {
inputExists = this.getInput('SECONDS');
} catch (e) {
inputExists = null;
}
if (withSeconds) {
if (!inputExists) {
const _input = this.appendValueInput('SECONDS');
_input.appendField(Blockly.Translate('cron_builder_seconds'));
const wp = this.workspace;
setTimeout(__input => {
if (!__input.connection.isConnected()) {
const _shadow = wp.newBlock('text');
_shadow.setShadow(true);
_shadow.setFieldValue('*', 'TEXT');
_shadow.initSvg();
_shadow.render();
_shadow.outputConnection.connect(__input.connection);
}
}, 100, _input);
}
} else if (inputExists) {
this.removeInput('SECONDS');
}
},
};
Blockly.JavaScript.forBlock['cron_builder'] = function (block) {
const vDow = Blockly.JavaScript.valueToCode(block, 'DOW', Blockly.JavaScript.ORDER_ATOMIC);
const vMonths = Blockly.JavaScript.valueToCode(block, 'MONTHS', Blockly.JavaScript.ORDER_ATOMIC);
const vDays = Blockly.JavaScript.valueToCode(block, 'DAYS', Blockly.JavaScript.ORDER_ATOMIC);
const vHours = Blockly.JavaScript.valueToCode(block, 'HOURS', Blockly.JavaScript.ORDER_ATOMIC);
const vMinutes = Blockly.JavaScript.valueToCode(block, 'MINUTES', Blockly.JavaScript.ORDER_ATOMIC);
const fWithSeconds = block.getFieldValue('WITH_SECONDS');
let vSeconds = '0';
if (fWithSeconds) {
try {
vSeconds = Blockly.JavaScript.valueToCode(block, 'SECONDS', Blockly.JavaScript.ORDER_ATOMIC);
} catch (e) {
// If no seconds input exists, we set it to an empty string
}
}
const code =
(fWithSeconds === 'TRUE' || fWithSeconds === 'true' || fWithSeconds === true ?
vSeconds + `.toString().trim() + ' ' + ` : '') +
vMinutes + `.toString().trim() + ' ' + ` +
vHours + `.toString().trim() + ' ' + ` +
vDays + `.toString().trim() + ' ' + ` +
vMonths + `.toString().trim() + ' ' + ` +
vDow + '.toString().trim()';
return [code, Blockly.JavaScript.ORDER_ATOMIC];
};
// --- onMessage -----------------------------------------------------------
Blockly.Trigger.blocks['onMessage'] =
'<block type="onMessage">' +
' <field name="MESSAGE">customMessage</field>' +
'</block>';
Blockly.Blocks['onMessage'] = {
init: function () {
this.appendDummyInput('NAME')
.appendField('✉️ ' + Blockly.Translate('onMessage'));
this.appendDummyInput('MESSAGE')
.appendField(Blockly.Translate('onMessage_message'))
.appendField(new Blockly.FieldTextInput('customMessage'), 'MESSAGE');
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('onMessage_tooltip'));
this.setHelpUrl(getHelp('onMessage_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = true;
// Is the block nested in a trigger?
let block = this;
while (block = block.getSurroundParent()) {
if (block && Blockly.Trigger.WARNING_PARENTS.includes(block.type)) {
legal = false;
break;
}
}
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('trigger_in_trigger_warning'), this.id);
}
},
};
Blockly.JavaScript.forBlock['onMessage'] = function (block) {
const fMessage = block.getFieldValue('MESSAGE');
const statement = Blockly.JavaScript.statementToCode(block, 'STATEMENT');
return `onMessage(${Blockly.JavaScript.quote_(fMessage)}, async (data, callback) => {\n` +
statement +
Blockly.JavaScript.prefixLines(`typeof callback === 'function' && callback({ result: true }); // default callback`, Blockly.JavaScript.INDENT) + '\n' +
'});\n';
};
// --- onMessage_data -----------------------------------------------------------
Blockly.Trigger.blocks['onMessage_data'] =
'<sep gap="5"></sep>' +
'<block type="onMessage_data">' +
' <field name="ATTR">data</field>' +
'</block>';
Blockly.Blocks['onMessage_data'] = {
/**
* Block for conditionally returning a value from a procedure.
* @this Blockly.Block
*/
init: function () {
this.appendDummyInput()
.appendField('✉️ ');
this.appendDummyInput('ATTR')
.appendField(new Blockly.FieldDropdown([
[Blockly.Translate('onMessage_data_data'), 'data'],
]), 'ATTR');
this.setInputsInline(true);
this.setOutput(true);
this.setColour(Blockly.Action.HUE);
this.setTooltip(Blockly.Translate('onMessage_data_tooltip'));
this.setHelpUrl(getHelp('onMessage_data_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let legal = false;
// Is the block nested in a trigger?
let block = this;
do {
if (this.FUNCTION_TYPES.includes(block.type)) {
legal = true;
break;
}
block = block.getSurroundParent();
} while (block);
if (legal) {
this.setWarningText(null, this.id);
} else {
this.setWarningText(Blockly.Translate('onMessage_data_warning'), this.id);
}
},
/**
* List of block types that are functions and thus do not need warnings.
* To add a new function type add this to your code:
* Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func');
*/
FUNCTION_TYPES: ['onMessage'],
};
Blockly.JavaScript.forBlock['onMessage_data'] = function (block) {
const fAttr = block.getFieldValue('ATTR');
return [fAttr, Blockly.JavaScript.ORDER_ATOMIC];
};
// --- onFile -----------------------------------------------------------
Blockly.Trigger.blocks['onFile'] =
'<block type="onFile">' +
' <field name="WITH_FILE">FALSE</field>' +
' <value name="OID">' +
' <shadow type="field_oid_meta">' +
' <field name="oid">0_userdata.0</field>' +
' </shadow>' +
' </value>' +
' <value name="FILE">' +
' <shadow type="text">' +
' <field name="TEXT">*</field>' +
' </shadow>' +
' </value>' +
'</block>';
Blockly.Blocks['onFile'] = {
init: function () {
this.appendValueInput('OID')
.appendField('📁 ' + Blockly.Translate('onFile'))
.setCheck(null);
this.appendValueInput('FILE')
.appendField(Blockly.Translate('onFile_file'))
.setCheck(null);
this.appendDummyInput('WITH_FILE_INPUT')
.appendField(Blockly.Translate('onFile_withFile'))
.appendField(new Blockly.FieldCheckbox('FALSE'), 'WITH_FILE');
this.appendStatementInput('STATEMENT')
.setCheck(null);
this.setInputsInline(false);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour(Blockly.Trigger.HUE);
this.setTooltip(Blockly.Translate('onFile_tooltip'));
this.setHelpUrl(getHelp('onFile_help'));
},
/**
* Called whenever anything on the workspace changes.
* Add warning if this flow block is not nested inside a loop.
* @param {!Blockly.Events.Abstract} e Change event.
* @this Blockly.Block
*/
onchange: function (e) {
let le