Compare commits
No commits in common. "main" and "tylerdcooper-patch-4" have entirely different histories.
main
...
tylerdcoop
31 changed files with 308 additions and 851 deletions
3
.github/workflows/docs.yml
vendored
3
.github/workflows/docs.yml
vendored
|
|
@ -41,9 +41,6 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Export Block Images
|
|
||||||
run: npm run export:block-images
|
|
||||||
|
|
||||||
- name: Export Block Definitions to Markdown
|
- name: Export Block Definitions to Markdown
|
||||||
run: npm run docs:export
|
run: npm run docs:export
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,9 @@ Node v22.x is expected, but other versions may work.
|
||||||
git clone https://github.com/adafruit/io-actions
|
git clone https://github.com/adafruit/io-actions
|
||||||
cd io-actions
|
cd io-actions
|
||||||
npm i
|
npm i
|
||||||
npm run export:block-images # run once, and whenever images need updating
|
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Now 2 processes should be running:
|
|
||||||
- a builder process that:
|
|
||||||
- does a full build of the docs from the app files
|
|
||||||
- watches app files for changes and updates matching docs
|
|
||||||
- the docs dev server where you can see your changes rendered live as you save them
|
|
||||||
|
|
||||||
When you're done working simply press `CTRL + C` to terminate the processes.
|
|
||||||
|
|
||||||
### Exporting
|
### Exporting
|
||||||
|
|
||||||
Export a Blockly application:
|
Export a Blockly application:
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
import mutator from './if/mutator.js'
|
import mutator from './if/mutator.js'
|
||||||
|
|
||||||
|
|
||||||
/** @type {import('#types').BlockDefinitionRaw} */
|
/** @type {import('#types').BlockDefinitionRaw} */
|
||||||
export default {
|
export default {
|
||||||
type: 'io_controls_if',
|
type: 'io_controls_if',
|
||||||
bytecodeKey: "conditional",
|
bytecodeKey: "conditional",
|
||||||
name: "Conditional",
|
name: "Conditional",
|
||||||
description: "Create smart decision-making logic for your IoT Actions using if/then/else statements. Perfect for building automation like 'if temperature > 80°F then turn on fan, else if temperature < 60°F then turn on heater, else turn off both'. Essential for creating intelligent responses based on sensor data, time conditions, or any combination of factors.",
|
description: "Execute different block diagrams based on the outcome of conditional checks.",
|
||||||
colour: 60,
|
colour: 60,
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "statement",
|
mode: "statement",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
next: 'expression'
|
next: 'expression'
|
||||||
},
|
},
|
||||||
|
|
||||||
mutator,
|
mutator,
|
||||||
|
|
||||||
template: `
|
template: `
|
||||||
if %IF0
|
if %IF0
|
||||||
do %THEN0
|
do %THEN0
|
||||||
else if %ELSE_IF_LABEL
|
else if %ELSE_IF_LABEL
|
||||||
else %ELSE_LABEL
|
else %ELSE_LABEL
|
||||||
`,
|
`,
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
IF0: {
|
IF0: {
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_logic_boolean'
|
shadow: 'io_logic_boolean'
|
||||||
},
|
},
|
||||||
|
|
||||||
THEN0: {
|
THEN0: {
|
||||||
check: "expression",
|
check: "expression",
|
||||||
type: 'statement',
|
type: 'statement',
|
||||||
|
|
@ -40,85 +47,93 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ELSE_IF_LABEL: {
|
ELSE_IF_LABEL: {
|
||||||
type: 'label',
|
type: 'label',
|
||||||
},
|
},
|
||||||
|
|
||||||
ELSE_LABEL: {
|
ELSE_LABEL: {
|
||||||
type: 'label',
|
type: 'label',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
docOverrides: {
|
docOverrides: {
|
||||||
inputs: `
|
inputs: `
|
||||||
### \`If\`
|
### \`If\`
|
||||||
This condition will always be checked first. Connect comparison blocks, sensor checks, or any logic that results in true/false. If it evaluates to \`true\`, the actions in the corresponding 'do' section will execute, and the rest of the conditional block will be skipped. If \`false\`, execution moves to the next "else if" (if present), or the final "else" (if present).
|
This block tree will always be run. If it resolve to \`true\`, the blocks
|
||||||
|
under the next 'do' section will be executed. Otherwise, execution moves
|
||||||
**Examples:**
|
to the next "else if" (if present), or the final "else" (if present.)
|
||||||
- \`temperature > 80\` - Check if it's hot
|
|
||||||
- \`motion detected AND time after sunset\` - Security logic
|
|
||||||
- \`battery level < 15%\` - Low power warning
|
|
||||||
|
|
||||||
### \`Do\`
|
### \`Do\`
|
||||||
The actions to execute when the preceding "if" or "else if" condition is \`true\`. Connect any action blocks like sending emails, controlling devices, publishing to feeds, or logging data. These actions only run if their corresponding condition evaluates to true.
|
The block diagram to execute when the preceding "if" or "else if" clause
|
||||||
|
resolves to \`true\`.
|
||||||
**Examples:**
|
|
||||||
- Send alert email + turn on cooling fan
|
|
||||||
- Log warning message + publish backup data
|
|
||||||
- Turn on lights + send notification
|
|
||||||
|
|
||||||
### \`Else if\`
|
### \`Else if\`
|
||||||
**Optional:** Add additional conditions to test if the main "if" was false. Click the gear icon and select "+ else if" to add more branches. Each "else if" is only checked if all previous conditions were false. Perfect for handling multiple scenarios like different temperature ranges, various alert levels, or time-based alternatives.
|
**Optional:** "else if" only appears after clicking "+ else if", and can be
|
||||||
|
removed by clicking the "-" next to it.
|
||||||
|
|
||||||
**Examples:**
|
Another "if" to check, only if every prior if has executed and none
|
||||||
- \`else if temperature < 60\` → turn on heater
|
resolved to \`true\`.
|
||||||
- \`else if battery < 50%\` → reduce power consumption
|
|
||||||
- \`else if motion detected\` → different security response
|
|
||||||
|
|
||||||
### \`Else\`
|
### \`Else\`
|
||||||
**Optional:** The fallback section that executes when ALL "if" and "else if" conditions are false. Click the gear icon and select "+ else" to add this section. Perfect for default actions, error handling, or "normal operation" behavior.
|
**Optional:** "else" only appears after clicking "+ else", and can be removed
|
||||||
|
by clicking "-" next to it.
|
||||||
|
|
||||||
**Examples:**
|
This section will execute if all "if"s and "else-if"s have been executed and
|
||||||
- Turn off all climate control (temperature is in normal range)
|
all resolved to \`false\`.
|
||||||
- Send "all systems normal" status update
|
|
||||||
- Resume regular power consumption mode
|
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
conditional: {}
|
conditional: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = 0
|
let index = 0
|
||||||
while(block.getInput(`IF${index}`)) {
|
while(block.getInput(`IF${index}`)) {
|
||||||
const
|
const
|
||||||
ifClause = generator.valueToCode(block, `IF${index}`, 0) || 'null',
|
ifClause = generator.valueToCode(block, `IF${index}`, 0) || 'null',
|
||||||
thenClause = generator.statementToCode(block, `THEN${index}`) || ''
|
thenClause = generator.statementToCode(block, `THEN${index}`) || ''
|
||||||
|
|
||||||
payload.conditional[`if${index}`] = JSON.parse(ifClause)
|
payload.conditional[`if${index}`] = JSON.parse(ifClause)
|
||||||
payload.conditional[`then${index}`] = JSON.parse(`[ ${thenClause} ]`)
|
payload.conditional[`then${index}`] = JSON.parse(`[ ${thenClause} ]`)
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if(block.getInput('ELSE')) {
|
if(block.getInput('ELSE')) {
|
||||||
const elseClause = generator.statementToCode(block, 'ELSE') || ''
|
const elseClause = generator.statementToCode(block, 'ELSE') || ''
|
||||||
|
|
||||||
payload.conditional.else = JSON.parse(`[${ elseClause }]`)
|
payload.conditional.else = JSON.parse(`[${ elseClause }]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(payload)
|
return JSON.stringify(payload)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (bytecode, helpers) => {
|
json: (bytecode, helpers) => {
|
||||||
const payload = bytecode.conditional
|
const payload = bytecode.conditional
|
||||||
|
|
||||||
if(!payload) {
|
if(!payload) {
|
||||||
throw new Error("No data for io_controls_if regenerator")
|
throw new Error("No data for io_controls_if regenerator")
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputs = {}
|
const inputs = {}
|
||||||
|
|
||||||
let index = 0
|
let index = 0
|
||||||
while(payload[`if${index}`] || payload[`then${index}`]) {
|
while(payload[`if${index}`] || payload[`then${index}`]) {
|
||||||
inputs[`IF${index}`] = helpers.expressionToBlock(payload[`if${index}`], { shadow: 'io_logic_boolean' })
|
inputs[`IF${index}`] = helpers.expressionToBlock(payload[`if${index}`], { shadow: 'io_logic_boolean' })
|
||||||
inputs[`THEN${index}`] = helpers.arrayToStatements(payload[`then${index}`])
|
inputs[`THEN${index}`] = helpers.arrayToStatements(payload[`then${index}`])
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if(payload.else) {
|
if(payload.else) {
|
||||||
inputs.ELSE = helpers.arrayToStatements(payload.else)
|
inputs.ELSE = helpers.arrayToStatements(payload.else)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "io_controls_if",
|
type: "io_controls_if",
|
||||||
inputs,
|
inputs,
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,28 @@ export default {
|
||||||
type: 'io_logic_boolean',
|
type: 'io_logic_boolean',
|
||||||
name: "Boolean",
|
name: "Boolean",
|
||||||
colour: 60,
|
colour: 60,
|
||||||
description: "A simple true or false value for building logic conditions and controlling digital outputs. Use 'true' to turn things on, enable conditions, or represent 'yes' states. Use 'false' to turn things off, disable conditions, or represent 'no' states. Essential for controlling relays, LEDs, alarms, and any on/off IoT devices.",
|
description: "A true or false value.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "boolean" ],
|
output: [ "expression", "boolean" ],
|
||||||
},
|
},
|
||||||
|
|
||||||
template: "%BOOL",
|
template: "%BOOL",
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
BOOL: {
|
BOOL: {
|
||||||
description: "Choose the boolean state you want to use:",
|
|
||||||
options: [
|
options: [
|
||||||
['true', 'TRUE', "Represents 'on', 'yes', 'enabled', or 'active' state. Use for turning on devices, enabling features, or setting positive conditions."],
|
['true', 'TRUE'],
|
||||||
['false', 'FALSE', "Represents 'off', 'no', 'disabled', or 'inactive' state. Use for turning off devices, disabling features, or setting negative conditions."],
|
['false', 'FALSE'],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: block => {
|
json: block => {
|
||||||
const bool = block.getFieldValue('BOOL') === 'TRUE'
|
const bool = block.getFieldValue('BOOL') === 'TRUE'
|
||||||
|
|
||||||
return [ JSON.stringify(bool), 0 ]
|
return [ JSON.stringify(bool), 0 ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,23 @@ export default {
|
||||||
bytecodeKey: "negate",
|
bytecodeKey: "negate",
|
||||||
name: "Negate",
|
name: "Negate",
|
||||||
colour: 60,
|
colour: 60,
|
||||||
description: "Flip any condition to its opposite - turns true into false and false into true. Essential for creating inverse logic like 'if NOT raining', 'if door is NOT open', or 'if temperature is NOT above 75°F'. Perfect for building exception handling, safety conditions, and reverse automation logic in your IoT Actions.",
|
description: "Swaps a truthy value to `false`, or a falsy value to `true`.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
},
|
},
|
||||||
|
|
||||||
template: "not %EXPRESSION",
|
template: "not %EXPRESSION",
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
EXPRESSION: {
|
EXPRESSION: {
|
||||||
description: "Connect any condition or comparison that you want to reverse. Examples: attach 'temperature > 80' to create 'NOT temperature > 80' (meaning temperature ≤ 80), or 'motion detected' to create 'NOT motion detected' (meaning no motion).",
|
description: "Block diagram that will be resolved, then have its truthiness flipped.",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_logic_boolean'
|
shadow: 'io_logic_boolean'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
|
|
@ -26,12 +30,15 @@ export default {
|
||||||
target: JSON.parse(operand)
|
target: JSON.parse(operand)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ JSON.stringify(payload), 0 ]
|
return [ JSON.stringify(payload), 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const payload = blockObject.negate
|
const payload = blockObject.negate
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'io_logic_negate',
|
type: 'io_logic_negate',
|
||||||
inputs: {
|
inputs: {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export default {
|
||||||
fields = {
|
fields = {
|
||||||
OP: comparator?.toUpperCase()
|
OP: comparator?.toUpperCase()
|
||||||
},
|
},
|
||||||
inputs = {
|
inputs: {
|
||||||
A: helpers.expressionToBlock(left, { shadow: 'io_logic_boolean' }),
|
A: helpers.expressionToBlock(left, { shadow: 'io_logic_boolean' }),
|
||||||
B: helpers.expressionToBlock(right, { shadow: 'io_logic_boolean' }),
|
B: helpers.expressionToBlock(right, { shadow: 'io_logic_boolean' }),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,43 +5,51 @@ export default {
|
||||||
name: "Compare Numbers Matcher",
|
name: "Compare Numbers Matcher",
|
||||||
colour: 224,
|
colour: 224,
|
||||||
inputsInline: true,
|
inputsInline: true,
|
||||||
description: "Create smart triggers based on numerical sensor data and thresholds. Perfect for temperature alerts ('notify when above 80°F'), battery monitoring ('warn when below 20%'), humidity control ('turn on fan when over 60%'), or any sensor-based automation that depends on numerical comparisons.",
|
description: "Numerically compare the new Feed value with another number.",
|
||||||
|
|
||||||
connections: { mode: 'value', output: 'matcher' },
|
connections: { mode: 'value', output: 'matcher' },
|
||||||
|
|
||||||
template: "%OP %B",
|
template: "%OP %B",
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
OP: {
|
OP: {
|
||||||
description: "Choose how to compare your feed's numerical data:",
|
description: "Select a comparison to perform",
|
||||||
options: [
|
options: [
|
||||||
['=', 'EQ', "Exactly equal: Triggers when feed value equals your number (e.g., button state = 1, exact temperature reading = 72.5°F). Best for digital switches and precise measurements."],
|
['=', 'EQ', "True if the two numbers are equal"],
|
||||||
['\u2260', 'NEQ', "Not equal: Triggers when feed value is anything other than your number (e.g., not zero, sensor reading changed from baseline). Useful for detecting changes or non-specific states."],
|
['\u2260', 'NEQ', "True if the two numbers are not equal"],
|
||||||
['\u200F<', 'LT', "Less than: Triggers when feed value drops below your threshold (e.g., temperature < 60°F for heating alerts, battery < 15% for low power warnings)."],
|
['\u200F<', 'LT', "True if the Feed value is less than number B"],
|
||||||
['\u200F\u2264', 'LTE', "Less than or equal: Triggers when feed value is at or below threshold (e.g., humidity ≤ 30% for dry air alerts, speed ≤ 0 for stopped condition)."],
|
['\u200F\u2264', 'LTE', "True if the Feed value is less than or equal to number B"],
|
||||||
['\u200F>', 'GT', "Greater than: Triggers when feed value exceeds your threshold (e.g., temperature > 85°F for cooling alerts, motion count > 10 for high activity)."],
|
['\u200F>', 'GT', "True if the Feed value is greater than number B"],
|
||||||
['\u200F\u2265', 'GTE', "Greater than or equal: Triggers when feed value meets or exceeds threshold (e.g., pressure ≥ 1000 hPa for weather tracking, light level ≥ 500 for daylight detection)."],
|
['\u200F\u2265', 'GTE', "True if the Feed value is greater than or equal to number B"],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
B: {
|
B: {
|
||||||
description: "Set your comparison threshold or target value. Examples: 80 for temperature alerts, 20 for battery percentage warnings, 1000 for pressure readings, or any numerical value that's meaningful for your sensor data.",
|
description: "The value to compare with the Feed value.",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_math_number'
|
shadow: 'io_math_number'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
comparator = block.getFieldValue('OP'),
|
comparator = block.getFieldValue('OP'),
|
||||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||||
|
|
||||||
blockPayload = JSON.stringify({
|
blockPayload = JSON.stringify({
|
||||||
matcherCompare: {
|
matcherCompare: {
|
||||||
comparator: comparator?.toLowerCase() || null,
|
comparator: comparator?.toLowerCase() || null,
|
||||||
right: JSON.parse(rightExp),
|
right: JSON.parse(rightExp),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ blockPayload, 0 ]
|
return [ blockPayload, 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -52,6 +60,7 @@ export default {
|
||||||
inputs = {
|
inputs = {
|
||||||
B: helpers.expressionToBlock(right, { shadow: 'io_math_number' }),
|
B: helpers.expressionToBlock(right, { shadow: 'io_math_number' }),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'matcher_compare', fields, inputs }
|
return { type: 'matcher_compare', fields, inputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,36 +5,42 @@ export default {
|
||||||
name: "Arithmetic",
|
name: "Arithmetic",
|
||||||
colour: 120,
|
colour: 120,
|
||||||
inputsInline: true,
|
inputsInline: true,
|
||||||
description: "Perform mathematical calculations using sensor data, feed values, or any numbers in your Actions. Perfect for creating custom formulas like calculating averages, converting units (Celsius to Fahrenheit), computing percentages, or building complex calculations from multiple data sources.",
|
description: "Perform the specified arithmetic operation on two specified operands.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
},
|
},
|
||||||
|
|
||||||
template: `%A %OP %B`,
|
template: `%A %OP %B`,
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
A: {
|
A: {
|
||||||
description: "The first number in your calculation (left side). Examples: temperature reading (72.5), feed value, sensor data, or results from other calculations. Non-numeric values will be automatically converted to numbers where possible.",
|
description: "The left side of the operation. Will be coerced to a number",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_math_number'
|
shadow: 'io_math_number'
|
||||||
},
|
},
|
||||||
|
|
||||||
B: {
|
B: {
|
||||||
description: "The second number in your calculation (right side). Examples: conversion factors (1.8 for temp conversion), target values (75 for comparison), other sensor readings, or mathematical constants. Also automatically converted to numbers.",
|
description: "The right side of the operation. Will be coerced to a number",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_math_number'
|
shadow: 'io_math_number'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
OP: {
|
OP: {
|
||||||
description: "Choose the mathematical operation to perform:",
|
description: "The mathematical operation to perform.",
|
||||||
options: [
|
options: [
|
||||||
['+', 'ADD', "Addition: Combine two numbers (e.g., 25 + 5 = 30). For totaling values, adding offsets, or combining multiple sensor readings into sums."],
|
['+', 'ADD', "add two numbers"],
|
||||||
['-', 'MINUS', "Subtraction: Remove B from A (e.g., 30 - 5 = 25). For calculating differences, finding deltas between readings, or subtracting baseline values."],
|
['-', 'MINUS', "subtract number B from number A"],
|
||||||
['x', 'MULTIPLY', "Multiplication: A times B (e.g., 6 x 4 = 24). For unit conversions, scaling values, calculating areas/volumes, or applying multiplication factors."],
|
['x', 'MULTIPLY', "multiply two numbers"],
|
||||||
['/', 'DIVIDE', "Division: A divided by B (e.g., 20 ÷ 4 = 5). For calculating averages, ratios, percentages, or converting between different unit scales."],
|
['/', 'DIVIDE', "divide number A by number B"],
|
||||||
['^', 'POWER', "Exponentiation: A raised to the power of B (e.g., 2^3 = 8). For advanced calculations, exponential growth models, or complex mathematical formulas."],
|
['^', 'POWER', "raise number A to the power of number B"],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
|
|
@ -48,6 +54,7 @@ export default {
|
||||||
operator = block.getFieldValue('OP'),
|
operator = block.getFieldValue('OP'),
|
||||||
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
||||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||||
|
|
||||||
blockPayload = JSON.stringify({
|
blockPayload = JSON.stringify({
|
||||||
arithmetic: {
|
arithmetic: {
|
||||||
left: JSON.parse(leftExp),
|
left: JSON.parse(leftExp),
|
||||||
|
|
@ -57,9 +64,11 @@ export default {
|
||||||
right: JSON.parse(rightExp)
|
right: JSON.parse(rightExp)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ blockPayload, 0 ]
|
return [ blockPayload, 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -78,6 +87,7 @@ export default {
|
||||||
A: helpers.expressionToBlock(payload.left, { shadow: 'io_math_number' }),
|
A: helpers.expressionToBlock(payload.left, { shadow: 'io_math_number' }),
|
||||||
B: helpers.expressionToBlock(payload.right, { shadow: 'io_math_number' }),
|
B: helpers.expressionToBlock(payload.right, { shadow: 'io_math_number' }),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'io_math_arithmetic', fields, inputs }
|
return { type: 'io_math_arithmetic', fields, inputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,43 +6,50 @@ export default {
|
||||||
colour: 120,
|
colour: 120,
|
||||||
inputsInline: true,
|
inputsInline: true,
|
||||||
primaryCategory: "Math",
|
primaryCategory: "Math",
|
||||||
description: "Build mathematical conditions by comparing any two numerical values in your Action logic. Perfect for creating if/then statements like 'if temperature is greater than target temp', 'if battery level equals low threshold', or 'if sensor reading is between two values'. Works with feed data, variables, calculations, or any numerical inputs.",
|
description: "Numerically compare two given values using the selected math operation.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
},
|
},
|
||||||
|
|
||||||
template: `%A %OP %B`,
|
template: `%A %OP %B`,
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
A: {
|
A: {
|
||||||
description: "The first number to compare (left side). Can be sensor data, variable values, calculation results, or any numerical input. Text and other data types will be automatically converted to numbers where possible.",
|
description: "The left side of the comparison. Will be coerced to a number",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_math_number'
|
shadow: 'io_math_number'
|
||||||
},
|
},
|
||||||
|
|
||||||
B: {
|
B: {
|
||||||
description: "The second number to compare (right side). Can be threshold values, target numbers, other sensor readings, or any numerical data you want to compare against the first input. Also automatically converted to numbers.",
|
description: "The right side of the comparison. Will be coerced to a number",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_math_number'
|
shadow: 'io_math_number'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
OP: {
|
OP: {
|
||||||
description: "Choose the mathematical relationship to test between your two numbers:",
|
description: "The mathematical comparison to use.",
|
||||||
options: [
|
options: [
|
||||||
['=', 'EQ', "Exactly equal: True when both numbers are precisely the same (e.g., temperature = 72.0, useful for exact target matching or digital sensor states like 0/1)."],
|
['=', 'EQ', "True if the two numbers are equal"],
|
||||||
['\u2260', 'NEQ', "Not equal: True when the numbers are different by any amount (e.g., sensor reading ≠ error value, useful for detecting changes or valid readings)."],
|
['\u2260', 'NEQ', "True if the two numbers are not equal"],
|
||||||
['\u200F<', 'LT', "Less than: True when first number is smaller (e.g., temperature < comfort threshold, battery < critical level for alerts)."],
|
['\u200F<', 'LT', "True if number A is less than number B"],
|
||||||
['\u200F\u2264', 'LTE', "Less than or equal: True when first number is smaller or exactly equal (e.g., humidity ≤ 40% for dry conditions, speed ≤ 0 for stopped state)."],
|
['\u200F\u2264', 'LTE', "True if number A is less than or equal to number B"],
|
||||||
['\u200F>', 'GT', "Greater than: True when first number is larger (e.g., pressure > storm threshold, light level > daylight minimum for automation)."],
|
['\u200F>', 'GT', "True if number A is greater than number B"],
|
||||||
['\u200F\u2265', 'GTE', "Greater than or equal: True when first number is larger or exactly equal (e.g., temperature ≥ 75°F for cooling, count ≥ 10 for bulk processing)."],
|
['\u200F\u2265', 'GTE', "True if number A is greater than or equal to number B"],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
comparator = block.getFieldValue('OP'),
|
comparator = block.getFieldValue('OP'),
|
||||||
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
||||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||||
|
|
||||||
blockPayload = JSON.stringify({
|
blockPayload = JSON.stringify({
|
||||||
compare: {
|
compare: {
|
||||||
left: JSON.parse(leftExp),
|
left: JSON.parse(leftExp),
|
||||||
|
|
@ -50,9 +57,11 @@ export default {
|
||||||
right: JSON.parse(rightExp),
|
right: JSON.parse(rightExp),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ blockPayload, 0 ]
|
return [ blockPayload, 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -64,6 +73,7 @@ export default {
|
||||||
A: helpers.expressionToBlock(left, { shadow: 'io_math_number' }),
|
A: helpers.expressionToBlock(left, { shadow: 'io_math_number' }),
|
||||||
B: helpers.expressionToBlock(right, { shadow: 'io_math_number' }),
|
B: helpers.expressionToBlock(right, { shadow: 'io_math_number' }),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'io_logic_compare', fields, inputs }
|
return { type: 'io_logic_compare', fields, inputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ export default {
|
||||||
bytecodeKey: "constrain",
|
bytecodeKey: "constrain",
|
||||||
name: "Constrain",
|
name: "Constrain",
|
||||||
colour: 120,
|
colour: 120,
|
||||||
description: "Keep any number within specified minimum and maximum boundaries. If the input value is below the minimum, it becomes the minimum. If above the maximum, it becomes the maximum. Perfect for ensuring values stay within expected ranges, creating percentage bounds, or limiting user input to acceptable values.",
|
description: "Constrain a given number to fall within a given range.",
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "number" ],
|
output: "number",
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
Constrain %VALUE
|
Constrain %VALUE
|
||||||
|
|
@ -15,12 +15,12 @@ export default {
|
||||||
`,
|
`,
|
||||||
inputs: {
|
inputs: {
|
||||||
VALUE: {
|
VALUE: {
|
||||||
description: "The number to limit within your specified boundaries. Examples: user input values, calculation results, sensor readings, or any numerical data that might go outside your desired range.",
|
description: "The number to constrain within the specified range limits.",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: "io_math_number"
|
shadow: "io_math_number"
|
||||||
},
|
},
|
||||||
RANGE: {
|
RANGE: {
|
||||||
description: "The minimum and maximum limits for your value. Examples: (0,100) for percentages, (1,10) for rating scales, (-50,150) for temperature ranges, or any boundaries that make sense for your data. Values outside these limits get automatically adjusted to the nearest boundary.",
|
description: "The minimum and maximum bounds to limit the value within (values outside this range will be clamped to the nearest boundary).",
|
||||||
check: 'range',
|
check: 'range',
|
||||||
shadow: {
|
shadow: {
|
||||||
type: "math_range",
|
type: "math_range",
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ export default {
|
||||||
bytecodeKey: "mapValue",
|
bytecodeKey: "mapValue",
|
||||||
name: "Map",
|
name: "Map",
|
||||||
colour: 120,
|
colour: 120,
|
||||||
description: "Transform sensor readings and data values by scaling them from one number range to another. Essential for IoT projects that need to convert raw sensor data (like 0-1023 from Arduino analog pins) into meaningful units (like 0-100% humidity), or translate between different measurement systems. Perfect for normalizing data, creating percentage values, or adapting sensor outputs to match your specific needs.",
|
description: "Scale a value from one range of numbers to another",
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "number" ],
|
output: "number",
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
Map
|
Map
|
||||||
|
|
@ -17,13 +17,13 @@ export default {
|
||||||
`,
|
`,
|
||||||
inputs: {
|
inputs: {
|
||||||
VALUE: {
|
VALUE: {
|
||||||
description: "The raw number you want to convert to a different scale. Examples: sensor reading of 512 (from 0-1023 analog range), temperature of 25°C (for Fahrenheit conversion), or any numerical data that needs scaling to a new range.",
|
description: "The number to scale from the original range to the target range.",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
bytecodeProperty: "value",
|
bytecodeProperty: "value",
|
||||||
shadow: 'io_math_number'
|
shadow: 'io_math_number'
|
||||||
},
|
},
|
||||||
FROM_RANGE: {
|
FROM_RANGE: {
|
||||||
description: "The original scale that your input value currently represents. Examples: (0,1023) for Arduino analog sensors, (0,255) for RGB color values, (-40,125) for temperature sensor ranges, or (0,100) for percentage data that needs different scaling.",
|
description: "The original range that the input value comes from (e.g., sensor readings from 0 to 1023).",
|
||||||
check: 'range',
|
check: 'range',
|
||||||
bytecodeProperty: "from",
|
bytecodeProperty: "from",
|
||||||
shadow: {
|
shadow: {
|
||||||
|
|
@ -41,7 +41,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TO_RANGE: {
|
TO_RANGE: {
|
||||||
description: "The new scale you want to convert your value to. Examples: (0,100) for percentage displays, (0.0,1.0) for normalized values, (32,212) for Fahrenheit conversion, or any target range that matches your display needs or system requirements.",
|
description: "The target range to scale the value to (e.g., convert to 0.0-1.0 for percentages).",
|
||||||
check: 'range',
|
check: 'range',
|
||||||
bytecodeProperty: "to",
|
bytecodeProperty: "to",
|
||||||
shadow: {
|
shadow: {
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,39 @@ export default {
|
||||||
type: 'io_math_number',
|
type: 'io_math_number',
|
||||||
name: "Number",
|
name: "Number",
|
||||||
color: 120,
|
color: 120,
|
||||||
description: "Enter any numerical value for use in your IoT Actions - whole numbers, decimals, positive, or negative. Perfect for setting thresholds (like 75 for temperature alerts), target values (like 50% for humidity control), timer durations (like 300 seconds), or any numerical data your automation needs. The foundation for all mathematical operations and comparisons.",
|
|
||||||
|
description: "A numeric value, whole or decimal.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "number" ],
|
output: [ "expression", "number" ],
|
||||||
},
|
},
|
||||||
|
|
||||||
extensions: {
|
extensions: {
|
||||||
validateNumbers: ({ block }) => {
|
validateNumbers: ({ block }) => {
|
||||||
const numField = block.getField("NUM")
|
const numField = block.getField("NUM")
|
||||||
|
|
||||||
if(!numField) { throw new Error("NUM field missing on io_math_number?") }
|
if(!numField) { throw new Error("NUM field missing on io_math_number?") }
|
||||||
|
|
||||||
numField.setValidator(newValue => {
|
numField.setValidator(newValue => {
|
||||||
const parsed = Number(newValue)
|
const parsed = Number(newValue)
|
||||||
|
|
||||||
if(!parsed && parsed !== 0) {
|
if(!parsed && parsed !== 0) {
|
||||||
return null // failed to parse, signal validation failure
|
return null // failed to parse, signal validation failure
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return parsed// parsed fine, use the result
|
return parsed// parsed fine, use the result
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
template: " %NUM",
|
template: " %NUM",
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
NUM: {
|
NUM: { text: '0' }
|
||||||
description: "Enter your numerical value here. Examples: 75 (temperature threshold), 3.14159 (mathematical constant), -10 (negative temperature), 0.5 (percentage as decimal), or any number your automation requires. Validates input to ensure it's a proper number.",
|
|
||||||
text: '0'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: block => {
|
json: block => {
|
||||||
return [Number(block.getFieldValue('NUM')) || '0', 0]
|
return [Number(block.getFieldValue('NUM')) || '0', 0]
|
||||||
|
|
|
||||||
|
|
@ -4,40 +4,47 @@ export default {
|
||||||
bytecodeKey: "round",
|
bytecodeKey: "round",
|
||||||
name: "Round/Floor/Ceiling",
|
name: "Round/Floor/Ceiling",
|
||||||
color: 120,
|
color: 120,
|
||||||
description: "Convert decimal numbers to whole numbers using different rounding strategies. Perfect for cleaning up sensor readings (23.7°F → 24°F), creating clean display values (3.14159 → 3), or preparing data for systems that only accept integers. Choose round for normal rounding, floor for always rounding down, or ceiling for always rounding up.",
|
description: "Round a value to the nearest whole number via round, floor, or ceiling functions",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
},
|
},
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
VALUE: {
|
VALUE: {
|
||||||
description: "The decimal number you want to convert to a whole number. Examples: sensor readings like 72.8°F, calculated values like 3.14159, or any numerical data that needs to be simplified. Non-numeric values will be automatically converted to numbers where possible.",
|
description: "A value you'd like to round to a whole number. Will be coerced to a number.",
|
||||||
bytecodeProperty: "value",
|
bytecodeProperty: "value",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: "io_math_number"
|
shadow: "io_math_number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
OPERATION: {
|
OPERATION: {
|
||||||
description: "Choose how to convert your decimal to a whole number:",
|
description: "Select which rounding operation to perform on the input:",
|
||||||
options: [
|
options: [
|
||||||
["Round", "round", "Standard rounding: 0.5 or higher rounds up, below 0.5 rounds down (e.g., 23.7 → 24, 23.4 → 23, 23.5 → 24). Best for general use and displaying clean values."],
|
["Round", "round", "if .5 or higher: round up; otherwise round down"],
|
||||||
["Floor", "floor", "Always rounds down to the nearest whole number, regardless of decimal value (e.g., 23.9 → 23, 23.1 → 23). Perfect for counting items, time calculations, or when you need conservative estimates."],
|
["Floor", "floor", "rounds down"],
|
||||||
["Ceiling", "ceiling", "Always rounds up to the nearest whole number, regardless of decimal value (e.g., 23.1 → 24, 23.9 → 24). Great for capacity planning, ensuring minimum values, or safety margins."],
|
["Ceiling", "ceiling", "rounds up"],
|
||||||
],
|
],
|
||||||
bytecodeProperty: "operation",
|
bytecodeProperty: "operation",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
template: "%OPERATION %VALUE",
|
template: "%OPERATION %VALUE",
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
value = JSON.parse(generator.valueToCode(block, 'VALUE', 0)),
|
value = JSON.parse(generator.valueToCode(block, 'VALUE', 0)),
|
||||||
operation = block.getFieldValue('OPERATION'),
|
operation = block.getFieldValue('OPERATION'),
|
||||||
payload = { round: { value, operation } }
|
payload = { round: { value, operation } }
|
||||||
|
|
||||||
return [ JSON.stringify(payload), 0 ]
|
return [ JSON.stringify(payload), 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -48,6 +55,7 @@ export default {
|
||||||
fields = {
|
fields = {
|
||||||
OPERATION: operation,
|
OPERATION: operation,
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'io_math_round', inputs, fields }
|
return { type: 'io_math_round', inputs, fields }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default {
|
||||||
%TRIGGERS
|
%TRIGGERS
|
||||||
Actions: |LEFT
|
Actions: |LEFT
|
||||||
%EXPRESSIONS
|
%EXPRESSIONS
|
||||||
\u00A0
|
\u3164
|
||||||
`,
|
`,
|
||||||
inputs: {
|
inputs: {
|
||||||
TRIGGERS: {
|
TRIGGERS: {
|
||||||
|
|
|
||||||
|
|
@ -6,40 +6,47 @@ export default {
|
||||||
colour: 180,
|
colour: 180,
|
||||||
inputsInline: true,
|
inputsInline: true,
|
||||||
primaryCategory: "Logic",
|
primaryCategory: "Logic",
|
||||||
description: "Compare any two pieces of text or data to build conditional logic in your Actions. Perfect for creating if/then statements like 'if device status equals online', 'if user name is not guest', or 'if error message contains timeout'. Works with feed values, variables, user input, or any text-based data.",
|
description: "Compare two chunks of text for equality, inequality, or inclusion.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
},
|
},
|
||||||
|
|
||||||
template: `%A %OP %B`,
|
template: `%A %OP %B`,
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
A: {
|
A: {
|
||||||
description: "The first value to compare (left side). Can be feed data, variable content, user input, or any text. Numbers and other data types will be automatically converted to text for comparison.",
|
description: "The left side of the comparison. Will be coerced to a string",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_text'
|
shadow: 'io_text'
|
||||||
},
|
},
|
||||||
|
|
||||||
B: {
|
B: {
|
||||||
description: "The second value to compare (right side). Can be literal text like 'online', variable content, feed values, or any data you want to compare against the first input. Also automatically converted to text.",
|
description: "The right side of the comparison. Will be coerced to a string",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_text'
|
shadow: 'io_text'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
OP: {
|
OP: {
|
||||||
description: "Choose how to compare the two text inputs:",
|
description: "Select what kind of comparison to do:",
|
||||||
options: [
|
options: [
|
||||||
['=', 'EQ', "Exact match: Returns true only if both inputs are identical (e.g., 'online' = 'online' is true, but 'Online' = 'online' is false due to case sensitivity)."],
|
['=', 'EQ', "Returns true if the the inputs are the same."],
|
||||||
['\u2260', 'NEQ', "Not equal: Returns true if the inputs are different in any way (e.g., useful for 'if status is not offline' or 'if username is not empty' conditions)."],
|
['\u2260', 'NEQ', "Returns true if the inputs not the same."],
|
||||||
['includes', 'INC', "Contains: Returns true if the first input contains the second input anywhere within it (e.g., 'sensor error timeout' includes 'error' would be true)."],
|
['includes', 'INC', "Returns true if input A includes input B."],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
comparator = block.getFieldValue('OP'),
|
comparator = block.getFieldValue('OP'),
|
||||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||||
|
|
||||||
blockPayload = JSON.stringify({
|
blockPayload = JSON.stringify({
|
||||||
textCompare: {
|
textCompare: {
|
||||||
left: JSON.parse(leftExp),
|
left: JSON.parse(leftExp),
|
||||||
|
|
@ -47,9 +54,11 @@ export default {
|
||||||
right: JSON.parse(rightExp),
|
right: JSON.parse(rightExp),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ blockPayload, 0 ]
|
return [ blockPayload, 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -61,6 +70,7 @@ export default {
|
||||||
A: helpers.expressionToBlock(left, { shadow: "io_text" }),
|
A: helpers.expressionToBlock(left, { shadow: "io_text" }),
|
||||||
B: helpers.expressionToBlock(right, { shadow: "io_text" }),
|
B: helpers.expressionToBlock(right, { shadow: "io_text" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'text_compare', fields, inputs }
|
return { type: 'text_compare', fields, inputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,387 +5,46 @@ export default {
|
||||||
name: "Join Text",
|
name: "Join Text",
|
||||||
colour: 180,
|
colour: 180,
|
||||||
inputsInline: true,
|
inputsInline: true,
|
||||||
description: `
|
description: "Join two pieces of text into one.",
|
||||||
Join two pieces of text into one combined string. Perfect for building dynamic messages, creating formatted outputs, or combining data from different sources into readable text.
|
|
||||||
|
|
||||||
## What is Join Text?
|
|
||||||
|
|
||||||
Think of Join Text like using a plus sign (+) to glue two pieces of text together. Just like "Hello" + "World" becomes "HelloWorld", this block combines any two text values into one.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
::: info
|
|
||||||
**Input A** + **Input B** → **Combined Output**
|
|
||||||
|
|
||||||
Example: \`"Hello "\` + \`"World"\` → \`"Hello World"\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
The Join Text block takes two inputs (A and B) and combines them into a single text output:
|
|
||||||
- **Input A**: First piece of text
|
|
||||||
- **Input B**: Second piece of text
|
|
||||||
- **Output**: A + B (combined text)
|
|
||||||
|
|
||||||
::: warning Important!
|
|
||||||
The texts are joined directly with **no space** between them. If you want a space, you need to add it yourself!
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Basic Examples
|
|
||||||
|
|
||||||
### Simple Text Joining
|
|
||||||
|
|
||||||
| Input A | Input B | Output |
|
|
||||||
|---------|---------|--------|
|
|
||||||
| \`"Hello"\` | \`"World"\` | \`"HelloWorld"\` |
|
|
||||||
| \`"Hello "\` | \`"World"\` | \`"Hello World"\` |
|
|
||||||
| \`"Temperature: "\` | \`"72°F"\` | \`"Temperature: 72°F"\` |
|
|
||||||
|
|
||||||
### Adding Spaces and Punctuation
|
|
||||||
|
|
||||||
| Input A | Input B | Output |
|
|
||||||
|---------|---------|--------|
|
|
||||||
| \`"Good"\` | \`" morning!"\` | \`"Good morning!"\` |
|
|
||||||
| \`"Status:"\` | \`" Active"\` | \`"Status: Active"\` |
|
|
||||||
| \`"User"\` | \`"#1234"\` | \`"User#1234"\` |
|
|
||||||
|
|
||||||
## IoT Use Cases
|
|
||||||
|
|
||||||
### 🏷️ Creating Labels with Values
|
|
||||||
|
|
||||||
**Temperature Label:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Kitchen Temp: "
|
|
||||||
B: {{ feeds['sensors.kitchen_temp'].value }}
|
|
||||||
// Output: "Kitchen Temp: 72.5"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Device Status:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Door is "
|
|
||||||
B: {{ feeds['security.door'].value }}
|
|
||||||
// Output: "Door is OPEN" or "Door is CLOSED"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 📊 Building Status Messages
|
|
||||||
|
|
||||||
**Battery Level:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Battery at "
|
|
||||||
B: {{ vars.battery_percent }}%
|
|
||||||
// Output: "Battery at 85%"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Sensor with Units:**
|
|
||||||
\`\`\`js
|
|
||||||
A: {{ feeds['sensors.humidity'].value }}
|
|
||||||
B: "% humidity"
|
|
||||||
// Output: "65% humidity"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 👤 Personalizing Messages
|
|
||||||
|
|
||||||
**Welcome Message:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Welcome back, "
|
|
||||||
B: {{ user.name }}
|
|
||||||
// Output: "Welcome back, John Smith"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Custom Greeting:**
|
|
||||||
\`\`\`js
|
|
||||||
A: {{ vars.greeting }}
|
|
||||||
B: {{ user.username }}
|
|
||||||
// Output: "Hello, jsmith123"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### 🔗 Creating URLs and Paths
|
|
||||||
|
|
||||||
::: code-group
|
|
||||||
|
|
||||||
\`\`\`js [API Endpoint]
|
|
||||||
A: "https://api.example.com/sensors/"
|
|
||||||
B: {{ vars.sensor_id }}
|
|
||||||
// Output: "https://api.example.com/sensors/temp_01"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
\`\`\`js [File Path]
|
|
||||||
A: "/data/"
|
|
||||||
B: {{ vars.filename }}
|
|
||||||
// Output: "/data/readings_2024.csv"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 🎨 Formatting with Symbols
|
|
||||||
|
|
||||||
\`\`\`js
|
|
||||||
// Arrow Indicators
|
|
||||||
A: "Temperature "
|
|
||||||
B: "↑ Rising"
|
|
||||||
// Output: "Temperature ↑ Rising"
|
|
||||||
|
|
||||||
// Status Icons
|
|
||||||
A: "🔋 "
|
|
||||||
B: {{ vars.battery_status }}
|
|
||||||
// Output: "🔋 Charging" or "🔋 Full"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Advanced Patterns
|
|
||||||
|
|
||||||
::: details Chaining Multiple Join Blocks
|
|
||||||
Sometimes you need to combine more than two pieces of text. Use multiple Join Text blocks:
|
|
||||||
|
|
||||||
**Three-Part Message:**
|
|
||||||
\`\`\`js
|
|
||||||
// First Join
|
|
||||||
A: "Hello, "
|
|
||||||
B: {{ user.name }}
|
|
||||||
// Result: "Hello, John"
|
|
||||||
|
|
||||||
// Second Join (using first result)
|
|
||||||
A: [First Join Output]
|
|
||||||
B: "! Welcome back."
|
|
||||||
// Final Output: "Hello, John! Welcome back."
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Building Complex Messages
|
|
||||||
**Multi-Line Status Report (using \\n for line breaks):**
|
|
||||||
\`\`\`js
|
|
||||||
// First Join
|
|
||||||
A: "System Status\\n"
|
|
||||||
B: "Temperature: OK\\n"
|
|
||||||
|
|
||||||
// Second Join
|
|
||||||
A: [First Join]
|
|
||||||
B: "Humidity: OK\\n"
|
|
||||||
|
|
||||||
// Third Join
|
|
||||||
A: [Second Join]
|
|
||||||
B: "Battery: Low"
|
|
||||||
|
|
||||||
/* Output:
|
|
||||||
System Status
|
|
||||||
Temperature: OK
|
|
||||||
Humidity: OK
|
|
||||||
Battery: Low
|
|
||||||
*/
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Conditional Text Building
|
|
||||||
Combine with logic blocks to build different messages:
|
|
||||||
\`\`\`js
|
|
||||||
A: "Sensor "
|
|
||||||
B: [If temperature > 80 then "⚠️ HOT" else "✓ Normal"]
|
|
||||||
// Output: "Sensor ⚠️ HOT" or "Sensor ✓ Normal"
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Common Patterns & Tips
|
|
||||||
|
|
||||||
::: details 1. Don't Forget Spaces!
|
|
||||||
::: danger Common Mistake
|
|
||||||
Forgetting to add spaces is the #1 beginner error!
|
|
||||||
|
|
||||||
**Wrong:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Hello"
|
|
||||||
B: "World"
|
|
||||||
// Output: "HelloWorld" ❌
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Right:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Hello " // Space at the end
|
|
||||||
B: "World"
|
|
||||||
// Output: "Hello World" ✅
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Also Right:**
|
|
||||||
\`\`\`js
|
|
||||||
A: "Hello"
|
|
||||||
B: " World" // Space at the beginning
|
|
||||||
// Output: "Hello World" ✅
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details 2. Adding Separators
|
|
||||||
::: v-pre
|
|
||||||
|
|
||||||
| Separator | Input A | Input B | Output |
|
|
||||||
|-----------|---------|---------|--------|
|
|
||||||
| **Comma** | \`{{ vars.city }}\` | \`", USA"\` | \`"Boston, USA"\` |
|
|
||||||
| **Dash** | \`{{ vars.date }}\` | \`" - Event"\` | \`"2024-01-15 - Event"\` |
|
|
||||||
| **Colon** | \`"Error"\` | \`": Connection lost"\` | \`"Error: Connection lost"\` |
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details 3. Building Lists
|
|
||||||
\`\`\`js
|
|
||||||
// Bullet Point
|
|
||||||
A: "• "
|
|
||||||
B: {{ vars.item_name }}
|
|
||||||
// Output: "• Temperature Sensor"
|
|
||||||
|
|
||||||
// Numbered
|
|
||||||
A: "1. "
|
|
||||||
B: {{ vars.first_step }}
|
|
||||||
// Output: "1. Check connections"
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details 4. Combining Numbers and Text
|
|
||||||
When joining numbers with text, the number is automatically converted:
|
|
||||||
\`\`\`js
|
|
||||||
A: "Count: "
|
|
||||||
B: {{ vars.sensor_count }}
|
|
||||||
// Output: "Count: 5"
|
|
||||||
|
|
||||||
A: {{ feeds['sensor'].value }}
|
|
||||||
B: " degrees"
|
|
||||||
// Output: "72.5 degrees"
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details 5. Empty String Handling
|
|
||||||
If one input is empty, you get just the other input:
|
|
||||||
\`\`\`js
|
|
||||||
A: "Hello"
|
|
||||||
B: ""
|
|
||||||
// Output: "Hello"
|
|
||||||
|
|
||||||
A: ""
|
|
||||||
B: "World"
|
|
||||||
// Output: "World"
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Working with Other Blocks
|
|
||||||
|
|
||||||
::: details Join + Text Template
|
|
||||||
Use Join Text to prepare strings for templates:
|
|
||||||
\`\`\`js
|
|
||||||
// Join to create feed key
|
|
||||||
A: "sensor."
|
|
||||||
B: {{ vars.location }}
|
|
||||||
// Output: "sensor.kitchen"
|
|
||||||
|
|
||||||
// Then use in template
|
|
||||||
{{ feeds['[Join Output]'].value }}
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Join + Variables
|
|
||||||
Store joined text in a variable for reuse:
|
|
||||||
\`\`\`js
|
|
||||||
// Join
|
|
||||||
A: "Alert: "
|
|
||||||
B: {{ vars.message }}
|
|
||||||
|
|
||||||
// Store result in variable: "formatted_alert"
|
|
||||||
// Use later in multiple places
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Join + Conditionals
|
|
||||||
Build different messages based on conditions:
|
|
||||||
\`\`\`js
|
|
||||||
A: "Status: "
|
|
||||||
B: [If block result]
|
|
||||||
// Where If block returns "Online" or "Offline"
|
|
||||||
// Output: "Status: Online" or "Status: Offline"
|
|
||||||
\`\`\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
::: details Problem: Text runs together without spaces
|
|
||||||
**Solution:** Add a space at the end of the first text or beginning of the second text:
|
|
||||||
- Change \`"Hello"\` to \`"Hello "\`
|
|
||||||
- Or change \`"World"\` to \`" World"\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Problem: Getting [object Object] in output
|
|
||||||
::: v-pre
|
|
||||||
**Solution:** Make sure you're passing text/string values, not complex objects. Use the .value or .name property of feeds:
|
|
||||||
- Wrong: \`B: {{ feeds['sensor.temp'] }}\`
|
|
||||||
- Right: \`B: {{ feeds['sensor.temp'].value }}\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Problem: Numbers not displaying correctly
|
|
||||||
::: v-pre
|
|
||||||
**Solution:** Numbers are automatically converted to text. For formatting, use a Text Template block first:
|
|
||||||
- Basic: \`A: "Price: $" B: {{ vars.price }}\` → \`"Price: $9.99"\`
|
|
||||||
- Formatted: Use template with \`{{ vars.price | round: 2 }}\` first
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details Problem: Special characters not showing
|
|
||||||
**Solution:** Use Unicode or HTML entities:
|
|
||||||
- Degree symbol: Use \`"°"\` or \`"°"\`
|
|
||||||
- Line break: Use \`"\\n"\`
|
|
||||||
- Tab: Use \`"\\t"\`
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
::: details Task Reference Table
|
|
||||||
| Task | Input A | Input B | Output |
|
|
||||||
|------|---------|---------|--------|
|
|
||||||
| Add label | \`"Temp: "\` | \`"72°F"\` | \`"Temp: 72°F"\` |
|
|
||||||
| Add units | \`"50"\` | \`"%"\` | \`"50%"\` |
|
|
||||||
| Add prefix | \`"Error: "\` | \`message\` | \`"Error: [message]"\` |
|
|
||||||
| Add suffix | \`filename\` | \`".txt"\` | \`"[filename].txt"\` |
|
|
||||||
| Join names | \`first_name\` | \`last_name\` | \`"[first][last]"\` (no space!) |
|
|
||||||
| With space | \`first_name + " "\` | \`last_name\` | \`"[first] [last]"\` |
|
|
||||||
| Line break | \`"Line 1\\n"\` | \`"Line 2"\` | Two lines |
|
|
||||||
| Build path | \`"/home/"\` | \`username\` | \`"/home/[username]"\` |
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: details When to Use Join Text vs Text Template
|
|
||||||
**Use Join Text when:**
|
|
||||||
- Combining exactly two pieces of text
|
|
||||||
- Simple concatenation without complex formatting
|
|
||||||
- Building URLs, paths, or IDs
|
|
||||||
- Adding prefixes or suffixes
|
|
||||||
|
|
||||||
**Use Text Template when:**
|
|
||||||
- Combining more than two pieces of text
|
|
||||||
- Need advanced formatting (dates, numbers)
|
|
||||||
- Using conditional logic
|
|
||||||
- Creating multi-line messages
|
|
||||||
- Need more control over output format
|
|
||||||
:::
|
|
||||||
`,
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: "expression",
|
output: "expression",
|
||||||
},
|
},
|
||||||
|
|
||||||
template: "%A + %B",
|
template: "%A + %B",
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
A: {
|
A: {
|
||||||
description: "The first string of text - this will appear first in the combined output. Can be static text like 'Hello' or dynamic data from feeds/variables. Don't forget to add a space at the end if you want separation from the second text!",
|
description: "The first string of text",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: "io_text"
|
shadow: "io_text"
|
||||||
},
|
},
|
||||||
|
|
||||||
B: {
|
B: {
|
||||||
description: "The second string of text - this will appear immediately after the first text with no automatic spacing. Can be static text or dynamic values. Add a space at the beginning if you need separation from the first text.",
|
description: "The last string of text",
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: "io_text"
|
shadow: "io_text"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||||
|
|
||||||
blockPayload = JSON.stringify({
|
blockPayload = JSON.stringify({
|
||||||
textJoin: {
|
textJoin: {
|
||||||
left: JSON.parse(leftExp),
|
left: JSON.parse(leftExp),
|
||||||
right: JSON.parse(rightExp),
|
right: JSON.parse(rightExp),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ blockPayload, 0 ]
|
return [ blockPayload, 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -394,6 +53,7 @@ export default {
|
||||||
A: helpers.expressionToBlock(left, { shadow: "io_text" }),
|
A: helpers.expressionToBlock(left, { shadow: "io_text" }),
|
||||||
B: helpers.expressionToBlock(right, { shadow: "io_text" }),
|
B: helpers.expressionToBlock(right, { shadow: "io_text" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'io_text_join', inputs }
|
return { type: 'io_text_join', inputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,204 +6,71 @@ export default {
|
||||||
colour: 180,
|
colour: 180,
|
||||||
inputsInline: true,
|
inputsInline: true,
|
||||||
description: `
|
description: `
|
||||||
Create dynamic, personalized messages by combining static text with live data from your IoT system. <span v-pre>Perfect for intelligent notifications like "Hello {{ user.name }}, your temperature sensor read {{ feeds['sensors.temperature'].value }}°F" or automated reports that include current sensor values, user information, and real-time data. Uses the powerful Liquid templating language for advanced formatting and logic.</span>
|
Render a text template.
|
||||||
|
|
||||||
::: v-pre
|
::: v-pre
|
||||||
## What is a Text Template?
|
Special forms surrounded by {{ curly_braces }}
|
||||||
|
will be replaced with their current value during the action run.
|
||||||
Think of a text template like a form letter where you leave blanks to fill in with specific information. Instead of writing "Dear _____", you write "Dear {{ user.name }}" and the system automatically fills in the actual name when the message is sent.
|
|
||||||
|
These templates use the Liquid templating language, from Shopify, and many
|
||||||
## How It Works
|
helpful functions come built-in. See the
|
||||||
|
[Liquid Documentation](https://shopify.github.io/liquid/basics/introduction/)
|
||||||
This block renders (processes) a text template, replacing special placeholders with actual values.
|
to learn more.
|
||||||
Anything surrounded by {{ double curly braces }} gets replaced with real data when your action runs.
|
|
||||||
|
### Template Variables:
|
||||||
For example:
|
|
||||||
- Template: "Hello {{ user.name }}!"
|
\`{{ variables.var_name }}\` - get the value of a variable you have defined
|
||||||
- Becomes: "Hello John!"
|
with name 'var_name'
|
||||||
|
|
||||||
These templates use the Liquid templating language from Shopify, which includes many helpful built-in functions.
|
\`{{ vars.var_name }}\` - shorthand for same as above
|
||||||
Learn more: [Liquid Documentation](https://shopify.github.io/liquid/basics/introduction/)
|
|
||||||
|
\`{{ variables['var name'] }}\` - get the value of a variable you have
|
||||||
## Template Variables Reference
|
defined with name 'var name' (allows spaces in variable names
|
||||||
|
|
||||||
### User Information
|
\`{{ vars['var name'] }}\` - shorthand for same as above
|
||||||
- \`{{ user.name }}\` - Your full name (e.g., "John Smith")
|
|
||||||
- \`{{ user.username }}\` - Your username (e.g., "jsmith123")
|
\`{{ user.name }}\` - your user's name
|
||||||
|
|
||||||
### Custom Variables
|
\`{{ user.username }}\` - your user's username
|
||||||
- \`{{ variables.var_name }}\` - Get value of variable named 'var_name'
|
|
||||||
- \`{{ vars.var_name }}\` - Shorthand version (same as above)
|
\`{{ feeds['group_key.feed_key'].name }}\` - access a feed with key
|
||||||
- \`{{ variables['var name'] }}\` - Use brackets for names with spaces
|
'group_key.feed_key' and get its name
|
||||||
- \`{{ vars['my special var'] }}\` - Shorthand with spaces
|
|
||||||
|
\`{{ feeds[...].key }}\` - ...get its key
|
||||||
### Feed Data (Sensors & Devices)
|
|
||||||
- \`{{ feeds['group_key.feed_key'].value }}\` - Current value from a feed
|
\`{{ feeds[...].value }}\` - ...get its last value
|
||||||
- \`{{ feeds['group_key.feed_key'].name }}\` - Feed's display name
|
|
||||||
- \`{{ feeds['group_key.feed_key'].key }}\` - Feed's unique key
|
|
||||||
- \`{{ feeds['group_key.feed_key'].updated_at }}\` - Last update timestamp
|
|
||||||
|
|
||||||
## Real-World IoT Examples
|
|
||||||
|
|
||||||
### 🌡️ Temperature Alerts
|
|
||||||
**Basic Alert:**
|
|
||||||
\`"Temperature Alert: {{ feeds['sensors.temp'].value }}°F detected!"\`
|
|
||||||
Output: "Temperature Alert: 85°F detected!"
|
|
||||||
|
|
||||||
**Detailed Alert with Location:**
|
|
||||||
\`"⚠️ HIGH TEMP WARNING
|
|
||||||
Location: {{ feeds['sensors.temp'].name }}
|
|
||||||
Current: {{ feeds['sensors.temp'].value }}°F
|
|
||||||
Time: {{ feeds['sensors.temp'].updated_at }}
|
|
||||||
Please check your {{ vars.device_location }} immediately!"\`
|
|
||||||
|
|
||||||
### 📊 Daily Status Reports
|
|
||||||
**Simple Report:**
|
|
||||||
\`"Daily Report for {{ user.name }}:
|
|
||||||
• Temperature: {{ feeds['home.temp'].value }}°F
|
|
||||||
• Humidity: {{ feeds['home.humidity'].value }}%
|
|
||||||
• Battery: {{ vars.battery_level }}%"\`
|
|
||||||
|
|
||||||
**Comprehensive Home Report:**
|
|
||||||
\`"Good morning {{ user.name }}! Here's your home status:
|
|
||||||
|
|
||||||
🌡️ Climate Control:
|
|
||||||
- Living Room: {{ feeds['climate.living_temp'].value }}°F
|
|
||||||
- Bedroom: {{ feeds['climate.bedroom_temp'].value }}°F
|
|
||||||
- Humidity: {{ feeds['climate.humidity'].value }}%
|
|
||||||
|
|
||||||
🔋 Device Status:
|
|
||||||
- Door Sensor Battery: {{ feeds['security.door_battery'].value }}%
|
|
||||||
- Motion Detector: {{ vars.motion_status }}
|
|
||||||
- Last Activity: {{ feeds['security.last_motion'].updated_at }}
|
|
||||||
|
|
||||||
Have a great day!"\`
|
|
||||||
|
|
||||||
### 🚪 Security Notifications
|
|
||||||
**Door Alert:**
|
|
||||||
\`"🚪 {{ feeds['security.front_door'].name }} is {{ feeds['security.front_door'].value }}
|
|
||||||
Time: {{ feeds['security.front_door'].updated_at }}
|
|
||||||
User: {{ user.name }}"\`
|
|
||||||
|
|
||||||
### 🌱 Garden Monitoring
|
|
||||||
**Watering Reminder:**
|
|
||||||
\`"Hey {{ user.name }}, your {{ vars.plant_name }} needs attention!
|
|
||||||
Soil Moisture: {{ feeds['garden.moisture'].value }}%
|
|
||||||
Last Watered: {{ vars.last_water_date }}
|
|
||||||
Recommendation: Water {{ vars.water_amount }}ml"\`
|
|
||||||
|
|
||||||
### 🔔 Smart Doorbell Messages
|
|
||||||
**Visitor Notification:**
|
|
||||||
\`"{{ user.name }}, someone is at your door!
|
|
||||||
Camera: {{ feeds['doorbell.camera'].name }}
|
|
||||||
Motion Level: {{ feeds['doorbell.motion'].value }}
|
|
||||||
Time: {{ feeds['doorbell.motion'].updated_at }}"\`
|
|
||||||
|
|
||||||
## Advanced Liquid Features
|
|
||||||
|
|
||||||
### Conditional Logic
|
|
||||||
Use if/else statements for smart messages:
|
|
||||||
\`"{% if feeds['sensors.temp'].value > 80 %}
|
|
||||||
🔥 It's hot! {{ feeds['sensors.temp'].value }}°F
|
|
||||||
{% else %}
|
|
||||||
❄️ Nice and cool: {{ feeds['sensors.temp'].value }}°F
|
|
||||||
{% endif %}"\`
|
|
||||||
|
|
||||||
### Formatting Numbers
|
|
||||||
Round numbers or add decimal places:
|
|
||||||
\`"Temperature: {{ feeds['sensors.temp'].value | round: 1 }}°F"
|
|
||||||
"Battery: {{ vars.battery | round }}%"\`
|
|
||||||
|
|
||||||
### Date Formatting
|
|
||||||
Format timestamps for readability:
|
|
||||||
\`"Last updated: {{ feeds['sensor.temp'].updated_at | date: '%B %d at %I:%M %p' }}"\`
|
|
||||||
Output: "Last updated: January 15 at 03:45 PM"
|
|
||||||
|
|
||||||
### Math Operations
|
|
||||||
Perform calculations in templates:
|
|
||||||
\`"Temperature in Celsius: {{ feeds['sensors.temp'].value | minus: 32 | times: 5 | divided_by: 9 | round: 1 }}°C"\`
|
|
||||||
|
|
||||||
## Common Patterns & Tips
|
|
||||||
|
|
||||||
### 1. Fallback Values
|
|
||||||
Provide defaults when data might be missing:
|
|
||||||
\`"Temperature: {{ feeds['sensors.temp'].value | default: 'No reading' }}"\`
|
|
||||||
|
|
||||||
### 2. Multiple Feed Access
|
|
||||||
Combine data from multiple sources:
|
|
||||||
\`"Indoor: {{ feeds['home.inside_temp'].value }}°F
|
|
||||||
Outdoor: {{ feeds['home.outside_temp'].value }}°F
|
|
||||||
Difference: {{ feeds['home.inside_temp'].value | minus: feeds['home.outside_temp'].value }}°F"\`
|
|
||||||
|
|
||||||
### 3. Status Indicators
|
|
||||||
Use emoji based on values (keep in mind emoji will not work with displays):
|
|
||||||
\`"Battery: {{ vars.battery_level }}%
|
|
||||||
{% if vars.battery_level < 20 %}🔴{% elsif vars.battery_level < 50 %}🟡{% else %}🟢{% endif %}"\`
|
|
||||||
|
|
||||||
### 4. Time-Based Greetings
|
|
||||||
\`"{% assign hour = 'now' | date: '%H' | plus: 0 %}
|
|
||||||
{% if hour < 12 %}Good morning{% elsif hour < 17 %}Good afternoon{% else %}Good evening{% endif %}, {{ user.name }}!"\`
|
|
||||||
|
|
||||||
## Troubleshooting Common Issues
|
|
||||||
|
|
||||||
### Problem: Variable shows as blank
|
|
||||||
**Solution:** Check that:
|
|
||||||
- Variable name is spelled correctly (case-sensitive!)
|
|
||||||
- Feed key matches exactly (including group.feed format)
|
|
||||||
- Variable has been set before this template runs
|
|
||||||
|
|
||||||
### Problem: Template shows raw {{ }} text
|
|
||||||
**Solution:** Make sure:
|
|
||||||
- You're using double curly braces {{ }}
|
|
||||||
- No extra spaces inside braces
|
|
||||||
- Feed keys use square brackets and quotes: feeds['key']
|
|
||||||
|
|
||||||
### Problem: Timestamp looks weird
|
|
||||||
**Solution:** Format dates using Liquid filters:
|
|
||||||
\`{{ feeds['sensor'].updated_at | date: '%Y-%m-%d %H:%M' }}\`
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Keep it readable**: Use line breaks and spacing for multi-line messages
|
|
||||||
2. **Test with sample data**: Try your template with different values
|
|
||||||
3. **Use meaningful variable names**: 'kitchen_temp' is better than 'temp1'
|
|
||||||
4. **Add context**: Include units (°F, %, etc.) and location names
|
|
||||||
:::
|
:::
|
||||||
`,
|
`,
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "string" ],
|
output: [ "expression", "string" ],
|
||||||
},
|
},
|
||||||
|
|
||||||
template: "{{ %TEMPLATE",
|
template: "{{ %TEMPLATE",
|
||||||
|
|
||||||
inputs: {
|
inputs: {
|
||||||
TEMPLATE: {
|
TEMPLATE: {
|
||||||
description: `
|
|
||||||
::: v-pre
|
|
||||||
Create your template text with static content and dynamic {{ variable }} placeholders.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- 'Alert: {{ feeds['temp.kitchen'].value }}°F detected'
|
|
||||||
- 'Daily Report for {{ user.name }}: Battery at {{ vars.battery_level }}%'
|
|
||||||
|
|
||||||
Use {{ }} to insert live data into your message.
|
|
||||||
:::
|
|
||||||
`,
|
|
||||||
check: "expression",
|
check: "expression",
|
||||||
shadow: 'io_text_multiline'
|
shadow: 'io_text_multiline'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: (block, generator) => {
|
json: (block, generator) => {
|
||||||
const
|
const
|
||||||
template = generator.valueToCode(block, 'TEMPLATE', 0) || null,
|
template = generator.valueToCode(block, 'TEMPLATE', 0) || null,
|
||||||
|
|
||||||
blockPayload = JSON.stringify({
|
blockPayload = JSON.stringify({
|
||||||
textTemplate: {
|
textTemplate: {
|
||||||
template: JSON.parse(template)
|
template: JSON.parse(template)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ blockPayload, 0 ]
|
return [ blockPayload, 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
regenerators: {
|
regenerators: {
|
||||||
json: (blockObject, helpers) => {
|
json: (blockObject, helpers) => {
|
||||||
const
|
const
|
||||||
|
|
@ -211,6 +78,7 @@ export default {
|
||||||
inputs = {
|
inputs = {
|
||||||
TEMPLATE: helpers.expressionToBlock(template, { shadow: "io_text" })
|
TEMPLATE: helpers.expressionToBlock(template, { shadow: "io_text" })
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'text_template', inputs }
|
return { type: 'text_template', inputs }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,25 @@ export default {
|
||||||
type: "io_text",
|
type: "io_text",
|
||||||
name: "Text",
|
name: "Text",
|
||||||
colour: 180,
|
colour: 180,
|
||||||
description: "Enter any text content for use in your Actions - words, phrases, device commands, or messages. Perfect for setting device states ('on', 'off'), creating notification messages ('Alert: High temperature detected'), defining comparison values ('motion detected'), or sending commands to connected systems. The building block for all text-based automation and communication.",
|
description: "A String of text",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "string" ],
|
output: [ "expression", "string" ],
|
||||||
},
|
},
|
||||||
|
|
||||||
template: `"%TEXT`,
|
template: `"%TEXT`,
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
TEXT: {
|
TEXT: {
|
||||||
description: "Type your text content here. Examples: 'on' for device commands, 'High temperature alert!' for notifications, 'motion' for sensor state matching, email subjects like 'Daily Report', or any words/phrases your automation needs. Single line text only - use Multiline Text block for paragraphs.",
|
|
||||||
text: ''
|
text: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: block => {
|
json: block => {
|
||||||
const text = block.getFieldValue('TEXT')
|
const text = block.getFieldValue('TEXT')
|
||||||
|
|
||||||
return [ JSON.stringify(text), 0 ]
|
return [ JSON.stringify(text), 0 ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,25 @@ export default {
|
||||||
type: 'io_text_multiline',
|
type: 'io_text_multiline',
|
||||||
name: "Multiline Text",
|
name: "Multiline Text",
|
||||||
colour: 180,
|
colour: 180,
|
||||||
description: "Create formatted text content with multiple lines, paragraphs, and line breaks. Perfect for composing detailed email messages, creating formatted reports with sensor data, writing multi-paragraph notifications, or building structured text content that needs proper formatting and readability across multiple lines.",
|
description: "A String of longer-form text with newlines.",
|
||||||
|
|
||||||
connections: {
|
connections: {
|
||||||
mode: "value",
|
mode: "value",
|
||||||
output: [ "expression", "string" ],
|
output: [ "expression", "string" ],
|
||||||
},
|
},
|
||||||
|
|
||||||
template: "¶ %TEXT",
|
template: "¶ %TEXT",
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
TEXT: {
|
TEXT: {
|
||||||
description: "Enter your multi-line text content here. Use Enter/Return to create new lines and paragraphs. Perfect for email templates ('Dear User,\\n\\nYour temperature reading is...\\n\\nBest regards'), formatted reports, detailed notifications, or any text that needs structure and readability.",
|
|
||||||
multiline_text: ''
|
multiline_text: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
generators: {
|
generators: {
|
||||||
json: block => {
|
json: block => {
|
||||||
const text = block.getFieldValue('TEXT')
|
const text = block.getFieldValue('TEXT')
|
||||||
|
|
||||||
return [ JSON.stringify(text), 0 ]
|
return [ JSON.stringify(text), 0 ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
|
|
||||||
import process from 'node:process'
|
|
||||||
import { exec, execSync, spawnSync, spawn } from 'node:child_process'
|
|
||||||
import { promisify } from 'node:util'
|
|
||||||
|
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
|
||||||
|
|
||||||
// run a clean documentation build, wait for it to complete
|
|
||||||
console.log("Building docs from scratch...")
|
|
||||||
spawnSync("node", ["export.js", "docs"], { stdio: 'inherit' })
|
|
||||||
|
|
||||||
// start the file watcher and incremental builder
|
|
||||||
console.log("Starting incremental builder and file watcher...")
|
|
||||||
const docBuilder = spawn("node", ["--watch-path=./app", "export.js", "docs-incremental"], { stdio: 'inherit' })
|
|
||||||
docBuilder.on('error', err => console.log('Builder Error:', err))
|
|
||||||
docBuilder.on('exit', code => console.log('Builder Exited', code === 0 ? "Cleanly" : `With Error Code ${code}`))
|
|
||||||
|
|
||||||
// start the Vitepress docs dev server
|
|
||||||
console.log("Starting Vitepress docs server...")
|
|
||||||
const docServer = spawn("npm", ["run", "docs:dev"], { stdio: 'inherit' })
|
|
||||||
docServer.on('error', err => console.log('Server Error:', err))
|
|
||||||
docServer.on('exit', code => console.log('Server Exited', code === 0 ? "Cleanly" : `With Error Code ${code}`))
|
|
||||||
|
|
||||||
const killAll = () => {
|
|
||||||
console.log('Shutting down...')
|
|
||||||
docBuilder.kill()
|
|
||||||
docServer.kill()
|
|
||||||
process.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if either one exits, kill the other
|
|
||||||
console.log("Watching files for changes and servers for crashes")
|
|
||||||
docServer.on('exit', killAll)
|
|
||||||
docBuilder.on('exit', killAll)
|
|
||||||
|
|
@ -6,10 +6,6 @@ const REPO = 'https://github.com/adafruit/io-actions'
|
||||||
|
|
||||||
// https://vitepress.dev/reference/site-config
|
// https://vitepress.dev/reference/site-config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
vite: {
|
|
||||||
clearScreen: false
|
|
||||||
},
|
|
||||||
|
|
||||||
title: "IO Actions: Block Reference",
|
title: "IO Actions: Block Reference",
|
||||||
description: "Documentation for Adafruit IO's block-based Actions",
|
description: "Documentation for Adafruit IO's block-based Actions",
|
||||||
|
|
||||||
|
|
|
||||||
44
docs/test.md
44
docs/test.md
|
|
@ -1,44 +0,0 @@
|
||||||
# Markdown Tester
|
|
||||||
|
|
||||||
## Works
|
|
||||||
|
|
||||||
<span v-pre>
|
|
||||||
{{ span v-pre }}
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
what a warning!
|
|
||||||
:::
|
|
||||||
</span>
|
|
||||||
|
|
||||||
```
|
|
||||||
{{ triple_backticks }}
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
{{ js_triple_backticks }}
|
|
||||||
```
|
|
||||||
|
|
||||||
::: details Embedded in a panel
|
|
||||||
::: details multiple panels
|
|
||||||
::: details multiple panels
|
|
||||||
::: details multiple panels
|
|
||||||
a message!
|
|
||||||
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Doesn't Work
|
|
||||||
|
|
||||||
<span v-pre>
|
|
||||||
`{{ single_backticks }}`
|
|
||||||
</span>
|
|
||||||
|
|
||||||
## Experiment
|
|
||||||
|
|
||||||
::: v-pre
|
|
||||||
| Separator |
|
|
||||||
|-----------|
|
|
||||||
| <span v-pre>{{ in_span_table }}</span> |
|
|
||||||
| {{ in_span_table }} |
|
|
||||||
:::
|
|
||||||
|
|
||||||
64
export.js
64
export.js
|
|
@ -1,5 +1,5 @@
|
||||||
import { spawn, spawnSync } from 'node:child_process'
|
import { spawn, spawnSync } from 'node:child_process'
|
||||||
import { copyFileSync, cpSync, existsSync } from 'node:fs'
|
import { copyFileSync, cpSync } from 'node:fs'
|
||||||
|
|
||||||
import { cleanDir, write, totalBytesWritten } from "./export_util.js"
|
import { cleanDir, write, totalBytesWritten } from "./export_util.js"
|
||||||
import DefinitionSet from '#src/definitions/definition_set.js'
|
import DefinitionSet from '#src/definitions/definition_set.js'
|
||||||
|
|
@ -20,7 +20,6 @@ const
|
||||||
definitions = await DefinitionSet.load(),
|
definitions = await DefinitionSet.load(),
|
||||||
|
|
||||||
exporters = {
|
exporters = {
|
||||||
// Build the Blockly application itself
|
|
||||||
"app": async (destination="export") => {
|
"app": async (destination="export") => {
|
||||||
|
|
||||||
// clear the export directory
|
// clear the export directory
|
||||||
|
|
@ -35,13 +34,13 @@ const
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Build the documentation for the Blockly application
|
|
||||||
"docs": async () => {
|
"docs": async () => {
|
||||||
// TODO: check and warn if block images haven't been generated
|
// allow option to skip image generation
|
||||||
if(!existsSync("docs/block_images/action_root.png")) {
|
const skipImages = taskArgs.includes("skipImages")
|
||||||
console.log("Block images missing from docs/block_images!")
|
if(!skipImages) {
|
||||||
console.log("Run: `npm run export:block-images` before exporting the docs")
|
await exporters.blockImages()
|
||||||
process.exit(1)
|
cleanDir("docs/block_images")
|
||||||
|
cpSync("tmp/block_images/images", "docs/block_images", { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
await exporters.app("docs/blockly")
|
await exporters.app("docs/blockly")
|
||||||
|
|
@ -55,50 +54,31 @@ const
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// Build the documentation but only touch files that have changed
|
"blockImages": async () => {
|
||||||
// This is good to pair with a process that watches files for changes
|
const destination = "tmp/block_images"
|
||||||
"docs-incremental": async () => {
|
cleanDir(destination)
|
||||||
await exportTo("docs", definitions, exportItem => {
|
cleanDir(`${destination}/images`)
|
||||||
exportItem.blockIndex("blocks/index.md")
|
|
||||||
exportItem.blockPages()
|
|
||||||
exportItem.sidebar("blocks/_blocks_sidebar.json")
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// Create png images of all blocks by:
|
|
||||||
// - creating a temporary app with no toolbox and all blocks on the workspace
|
|
||||||
// - serving that application
|
|
||||||
// - driving a browser to it with Cypress
|
|
||||||
// - right clicking on each block and clicking "download image"
|
|
||||||
"blockImages": async (imageDestination="docs/block_images") => {
|
|
||||||
const tmpAppDestination = "tmp/block_images_app"
|
|
||||||
cleanDir(tmpAppDestination)
|
|
||||||
|
|
||||||
// export a special app with no toolbox, all blocks on workspace
|
// export a special app with no toolbox, all blocks on workspace
|
||||||
await exportTo(tmpAppDestination, definitions, exportItem => {
|
await exportTo(destination, definitions, exportItem => {
|
||||||
exportItem.workspaceAllBlocks("workspace.json")
|
exportItem.workspaceAllBlocks("workspace.json")
|
||||||
write(`${tmpAppDestination}/toolbox.json`, "null")
|
write(`${destination}/toolbox.json`, "null")
|
||||||
exportItem.blocks("blocks.json")
|
exportItem.blocks("blocks.json")
|
||||||
exportItem.script("blockly_app.js")
|
exportItem.script("blockly_app.js")
|
||||||
// TODO: make a DocumentExporter for generating html wrappers
|
// TODO: make a DocumentExporter for generating html wrappers
|
||||||
copyFileSync("src/exporters/document_templates/blockly_workspace.template.html", `${tmpAppDestination}/index.html`)
|
copyFileSync("src/exporters/document_templates/blockly_workspace.template.html", `${destination}/index.html`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// serve the screenshot app
|
// serve it
|
||||||
console.log('Serving workspace for screenshots...')
|
console.log('Serving workspace for screenshots...')
|
||||||
const viteProcess = spawn("npx", ["vite", "serve", tmpAppDestination])
|
const viteProcess = spawn("npx", ["vite", "serve", destination])
|
||||||
|
|
||||||
// prepare the image location
|
|
||||||
cleanDir(imageDestination)
|
|
||||||
|
|
||||||
// extract the screenshots
|
// extract the screenshots
|
||||||
console.log('Generating screenshots...')
|
console.log('Generating screenshots...')
|
||||||
await spawnSync("npx", ["cypress", "run",
|
spawnSync("npx", ["cypress", "run",
|
||||||
"--config", `downloadsFolder=${imageDestination}`,
|
"--config", `downloadsFolder=${destination}/images`,
|
||||||
"--config-file", `cypress/cypress.config.js`,
|
"--config-file", `cypress/cypress.config.js`,
|
||||||
"--browser", "chromium",
|
])
|
||||||
"--spec", "cypress/e2e/block_images.cy.js",
|
|
||||||
], { stdio: 'inherit' })
|
|
||||||
console.log('Generation complete.')
|
console.log('Generation complete.')
|
||||||
|
|
||||||
// kill the server
|
// kill the server
|
||||||
|
|
@ -106,19 +86,16 @@ const
|
||||||
console.log("Vite failed to exit gracefully")
|
console.log("Vite failed to exit gracefully")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Server closed.')
|
console.log('Server closed.')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exporterNames = Object.keys(exporters)
|
exporterNames = Object.keys(exporters)
|
||||||
|
|
||||||
// Look up the requested exporter
|
|
||||||
if(!exporterNames.includes(toExport)) {
|
if(!exporterNames.includes(toExport)) {
|
||||||
console.error(`Export Error: No exporter found for: "${toExport}"\nValid exporters: "${exporterNames.join('", "')}"`)
|
console.error(`Export Error: No exporter found for: "${toExport}"\nValid exporters: "${exporterNames.join('", "')}"`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the export
|
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
console.log(`\nStarting Export: ${toExport}`)
|
console.log(`\nStarting Export: ${toExport}`)
|
||||||
console.log("=======================")
|
console.log("=======================")
|
||||||
|
|
@ -128,8 +105,7 @@ await exporter()
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
console.log("=======================")
|
console.log("=======================")
|
||||||
console.log(`🏁 Done (${elapsed}ms) 🏁`)
|
console.log(`🏁 Done. Wrote ${totalBytesWritten.toFixed(3)}k in ${elapsed}ms 🏁`)
|
||||||
|
|
||||||
|
|
||||||
// Bye!
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,17 @@
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dev_server.js",
|
"start": "vite --force",
|
||||||
|
|
||||||
"test": "node --test",
|
"test": "node --test",
|
||||||
"test-watch": "node --test --watch",
|
"test-watch": "node --test --watch",
|
||||||
"test-snapshots": "node --test test/app/blocks/snapshots/block_snapshots_test.js",
|
"test-snapshots": "node --test test/app/blocks/snapshots/block_snapshots_test.js",
|
||||||
"test-update-snapshots": "node --test --test-update-snapshots test/app/blocks/snapshots/block_snapshots_test.js",
|
"test-update-snapshots": "node --test --test-update-snapshots test/app/blocks/snapshots/block_snapshots_test.js",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lint-export": "eslint export/",
|
"lint-export": "eslint export/",
|
||||||
|
|
||||||
"export:app": "node export.js app",
|
"export:app": "node export.js app",
|
||||||
"export:block-images": "node export.js blockImages",
|
"build": "npm run export:app && vite build",
|
||||||
|
"build-all-branches": "node build_all_branches.js",
|
||||||
|
"preview": "npm run build && vite preview",
|
||||||
"docs:export": "node export.js docs",
|
"docs:export": "node export.js docs",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
|
|
|
||||||
|
|
@ -191,11 +191,7 @@ BlockDefinition.parseRawDefinition = function(rawBlockDefinition, definitionPath
|
||||||
? niceTemplate(rawBlockDefinition.description)
|
? niceTemplate(rawBlockDefinition.description)
|
||||||
: ""
|
: ""
|
||||||
blockDef.ioPlus = rawBlockDefinition.ioPlus
|
blockDef.ioPlus = rawBlockDefinition.ioPlus
|
||||||
// take the first line of the description
|
blockDef.tooltip = blockDef.description.split("\n")[0]
|
||||||
// blockDef.tooltip = blockDef.description.split("\n")[0]
|
|
||||||
// take the first sentence of the description
|
|
||||||
blockDef.tooltip = blockDef.description.split(/\.(\s|$)/)[0]
|
|
||||||
if(!blockDef.tooltip.endsWith("?")) { blockDef.tooltip += "." }
|
|
||||||
blockDef.disabled = !!rawBlockDefinition.disabled
|
blockDef.disabled = !!rawBlockDefinition.disabled
|
||||||
blockDef.connections = rawBlockDefinition.connections
|
blockDef.connections = rawBlockDefinition.connections
|
||||||
blockDef.template = rawBlockDefinition.template
|
blockDef.template = rawBlockDefinition.template
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const
|
||||||
if(input.type === 'label') { return }
|
if(input.type === 'label') { return }
|
||||||
|
|
||||||
lines.push(`### \`${ capitalize(inputName) }\``)
|
lines.push(`### \`${ capitalize(inputName) }\``)
|
||||||
if(input.description) { lines.push(niceTemplate(input.description)) }
|
lines.push(input.description)
|
||||||
})
|
})
|
||||||
|
|
||||||
return lines.join("\n\n")
|
return lines.join("\n\n")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { mkdirSync, writeFileSync } from 'fs'
|
||||||
|
import { dirname } from 'path'
|
||||||
import { forEach, identity, pickBy } from 'lodash-es'
|
import { forEach, identity, pickBy } from 'lodash-es'
|
||||||
|
|
||||||
import { writeFileIfDifferent } from '#src/util.js'
|
|
||||||
import toBlockMarkdown from "#src/docs/render_block.js"
|
import toBlockMarkdown from "#src/docs/render_block.js"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,10 +25,10 @@ export default class BlockPageExporter {
|
||||||
forEach(this.definitionSet.blocks, blockDefinition => {
|
forEach(this.definitionSet.blocks, blockDefinition => {
|
||||||
const
|
const
|
||||||
docPath = options.filenameFunc(blockDefinition),
|
docPath = options.filenameFunc(blockDefinition),
|
||||||
fullPath = `${this.destination}/${docPath}`,
|
fullPath = `${this.destination}/${docPath}`
|
||||||
newContent = toBlockMarkdown(blockDefinition)
|
|
||||||
|
|
||||||
writeFileIfDifferent(fullPath, newContent)
|
mkdirSync(dirname(fullPath), { recursive: true })
|
||||||
|
writeFileSync(fullPath, toBlockMarkdown(blockDefinition))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,4 +41,3 @@ export default class BlockPageExporter {
|
||||||
this.export(exportOptions)
|
this.export(exportOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,52 +49,45 @@ export const
|
||||||
// do normal Blockly injection here
|
// do normal Blockly injection here
|
||||||
currentWorkspace = Blockly.inject(blocklyDivId, blocklyInjectOptions)
|
currentWorkspace = Blockly.inject(blocklyDivId, blocklyInjectOptions)
|
||||||
|
|
||||||
try {
|
// shortcut to make the outside data available everywhere/global
|
||||||
// shortcut to make the outside data available everywhere/global
|
// consider if this could be done other ways, less global
|
||||||
// consider if this could be done other ways, less global
|
currentWorkspace.extensionData = options.extensionData
|
||||||
currentWorkspace.extensionData = options.extensionData
|
|
||||||
|
|
||||||
registerToolboxCallbacks(currentWorkspace)
|
registerToolboxCallbacks(currentWorkspace)
|
||||||
|
|
||||||
if(options.disableOrphans) {
|
if(options.disableOrphans) {
|
||||||
currentWorkspace.addChangeListener(Blockly.Events.disableOrphans)
|
currentWorkspace.addChangeListener(Blockly.Events.disableOrphans)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.workspaceData) {
|
if(options.workspaceData) {
|
||||||
const workspaceJson = jsonToWorkspace(options.workspaceData)
|
const workspaceJson = jsonToWorkspace(options.workspaceData)
|
||||||
Blockly.serialization.workspaces.load(workspaceJson, currentWorkspace)
|
Blockly.serialization.workspaces.load(workspaceJson, currentWorkspace)
|
||||||
|
|
||||||
} else if(options.workspaceJson) {
|
} else if(options.workspaceJson) {
|
||||||
Blockly.serialization.workspaces.load(options.workspaceJson, currentWorkspace)
|
Blockly.serialization.workspaces.load(options.workspaceJson, currentWorkspace)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Blockly.serialization.workspaces.load(initialWorkspace, currentWorkspace)
|
Blockly.serialization.workspaces.load(initialWorkspace, currentWorkspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(options.onJsonUpdated || options.onJsonError) {
|
if(options.onJsonUpdated || options.onJsonError) {
|
||||||
// auto-regenerate code
|
// auto-regenerate code
|
||||||
currentWorkspace.addChangeListener(e => {
|
currentWorkspace.addChangeListener(e => {
|
||||||
if(e.isUiEvent || // no UI events
|
if(e.isUiEvent || // no UI events
|
||||||
e.type == Blockly.Events.FINISHED_LOADING || // no on-load
|
e.type == Blockly.Events.FINISHED_LOADING || // no on-load
|
||||||
currentWorkspace.isDragging()) // not while dragging
|
currentWorkspace.isDragging()) // not while dragging
|
||||||
{ return }
|
{ return }
|
||||||
|
|
||||||
// generate next cycle so orphans get disabled first
|
// generate next cycle so orphans get disabled first
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
const json = workspaceToJson(currentWorkspace)
|
const json = workspaceToJson(currentWorkspace)
|
||||||
options.onJsonUpdated?.(json)
|
options.onJsonUpdated?.(json)
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
options.onJsonError?.(error)
|
options.onJsonError?.(error)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
} catch(error) {
|
|
||||||
// clean things up
|
|
||||||
dispose()
|
|
||||||
// rethrow exception
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentWorkspace
|
return currentWorkspace
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { find, forEach, isString, map, sortBy } from 'lodash-es'
|
import { writeFileSync } from 'fs'
|
||||||
|
import { find, forEach, isString, map } from 'lodash-es'
|
||||||
import { writeFileIfDifferent } from '#src/util.js'
|
|
||||||
|
|
||||||
|
|
||||||
export default class SidebarExporter {
|
export default class SidebarExporter {
|
||||||
|
|
@ -35,7 +34,7 @@ export default class SidebarExporter {
|
||||||
|
|
||||||
blockSidebar.items.push(uncategorizedCategory)
|
blockSidebar.items.push(uncategorizedCategory)
|
||||||
|
|
||||||
forEach(sortBy(this.definitionSet.blocks, 'type'), blockDefinition => {
|
forEach(this.definitionSet.blocks, blockDefinition => {
|
||||||
const
|
const
|
||||||
sidebarEntry = {
|
sidebarEntry = {
|
||||||
text: blockDefinition.name,
|
text: blockDefinition.name,
|
||||||
|
|
@ -70,7 +69,7 @@ export default class SidebarExporter {
|
||||||
? options.toFile
|
? options.toFile
|
||||||
: `_blocks_sidebar.json`
|
: `_blocks_sidebar.json`
|
||||||
|
|
||||||
writeFileIfDifferent(`${this.destination}/${filename}`, JSON.stringify(blockSidebar, null, 2))
|
writeFileSync(`${this.destination}/${filename}`, JSON.stringify(blockSidebar, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
exportToFile = (toFile=true) => {
|
exportToFile = (toFile=true) => {
|
||||||
|
|
|
||||||
27
src/util.js
27
src/util.js
|
|
@ -1,21 +1,6 @@
|
||||||
import { dirname } from 'path'
|
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
||||||
import { createHash } from 'crypto'
|
|
||||||
|
|
||||||
import { map } from 'lodash-es'
|
import { map } from 'lodash-es'
|
||||||
|
|
||||||
|
|
||||||
const getStringHash = stringToHash => {
|
|
||||||
const hash = createHash('sha256')
|
|
||||||
hash.update(stringToHash)
|
|
||||||
return hash.digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFileHash = filePath => {
|
|
||||||
if(!existsSync(filePath)) { return '' }
|
|
||||||
return getStringHash(readFileSync(filePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const
|
export const
|
||||||
niceTemplate = tplString => {
|
niceTemplate = tplString => {
|
||||||
const
|
const
|
||||||
|
|
@ -34,16 +19,4 @@ export const
|
||||||
// TODO: support niceties for markdown, double-newlines, escaping, etc
|
// TODO: support niceties for markdown, double-newlines, escaping, etc
|
||||||
|
|
||||||
return tplString
|
return tplString
|
||||||
},
|
|
||||||
|
|
||||||
writeFileIfDifferent = (filename, content) => {
|
|
||||||
const
|
|
||||||
fileHash = getFileHash(filename),
|
|
||||||
contentHash = getStringHash(content)
|
|
||||||
|
|
||||||
if(fileHash !== contentHash) {
|
|
||||||
console.log("writing", filename)
|
|
||||||
mkdirSync(dirname(filename), { recursive: true })
|
|
||||||
writeFileSync(filename, content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ exports[`Block Snapshots > Blockly JSON > action_root 1`] = `
|
||||||
"inputsInline": false,
|
"inputsInline": false,
|
||||||
"type": "action_root",
|
"type": "action_root",
|
||||||
"colour": "0",
|
"colour": "0",
|
||||||
"tooltip": "The foundation of every Adafruit IO Action. Connect Triggers (like 'when temperature > 80°F' or 'every morning at 8 AM') to define when your Action runs, then attach Action blocks (like 'send email', 'publish to feed', or 'if/then logic') to define what happens when triggered.",
|
"tooltip": "Add Triggers to determine when this Action runs. Add Actions to determine what this Action does.",
|
||||||
"message0": "Triggers: %1",
|
"message0": "Triggers: %1",
|
||||||
"args0": [
|
"args0": [
|
||||||
{
|
{
|
||||||
|
|
@ -2522,7 +2522,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_compare 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "io_logic_compare",
|
"type": "io_logic_compare",
|
||||||
"colour": 120,
|
"colour": 120,
|
||||||
"tooltip": "Build mathematical conditions by comparing any two numerical values in your Action logic. Perfect for creating if/then statements like 'if temperature is greater than target temp', 'if battery level equals low threshold', or 'if sensor reading is between two values'. Works with feed data, variables, calculations, or any numerical inputs.",
|
"tooltip": "Numerically compare two given values using the selected math operation.",
|
||||||
"output": "expression",
|
"output": "expression",
|
||||||
"message0": "%1",
|
"message0": "%1",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -2581,7 +2581,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_negate 1`] = `
|
||||||
"inputsInline": false,
|
"inputsInline": false,
|
||||||
"type": "io_logic_negate",
|
"type": "io_logic_negate",
|
||||||
"colour": 60,
|
"colour": 60,
|
||||||
"tooltip": "Flip any condition to its opposite - turns true into false and false into true. Essential for creating inverse logic like 'if NOT raining', 'if door is NOT open', or 'if temperature is NOT above 75°F'. Perfect for building exception handling, safety conditions, and reverse automation logic in your IoT Actions.",
|
"tooltip": "Swaps a truthy value to \`false\`, or a falsy value to \`true\`.",
|
||||||
"output": "expression",
|
"output": "expression",
|
||||||
"message0": "not %1",
|
"message0": "not %1",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -2601,7 +2601,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_operation 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "io_logic_operation",
|
"type": "io_logic_operation",
|
||||||
"colour": 60,
|
"colour": 60,
|
||||||
"tooltip": "Combine multiple conditions to create sophisticated decision logic in your Actions. Perfect for complex automation like 'if temperature is high AND humidity is low', 'if motion detected OR door opened', or any scenario where you need multiple criteria to work together. Essential for building smart, multi-factor IoT control systems.",
|
"tooltip": "Perform the specifed boolean logic operation on two operands.",
|
||||||
"output": "expression",
|
"output": "expression",
|
||||||
"message0": "%1",
|
"message0": "%1",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -2876,7 +2876,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_get 1`] = `
|
||||||
"inputsInline": false,
|
"inputsInline": false,
|
||||||
"type": "io_variables_get",
|
"type": "io_variables_get",
|
||||||
"colour": 240,
|
"colour": 240,
|
||||||
"tooltip": "Retrieve the value stored in a variable that was previously set using a Set Variable block. Use this to access stored feed values, calculation results, or any data saved earlier in your Action.",
|
"tooltip": "Get the value previously assigned to a variable.",
|
||||||
"output": "expression",
|
"output": "expression",
|
||||||
"message0": "Get variable %1 %2",
|
"message0": "Get variable %1 %2",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -2898,7 +2898,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_set 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "io_variables_set",
|
"type": "io_variables_set",
|
||||||
"colour": 240,
|
"colour": 240,
|
||||||
"tooltip": "Store a value in a named variable for later use in your Action. Variables let you remember feed values, calculation results, or any data to use in subsequent action blocks.",
|
"tooltip": "Set a variable to a value",
|
||||||
"nextStatement": "expression",
|
"nextStatement": "expression",
|
||||||
"previousStatement": "expression",
|
"previousStatement": "expression",
|
||||||
"message0": "Set variable %1 = %2",
|
"message0": "Set variable %1 = %2",
|
||||||
|
|
@ -2923,7 +2923,7 @@ exports[`Block Snapshots > Blockly JSON > matcher_compare 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "matcher_compare",
|
"type": "matcher_compare",
|
||||||
"colour": 224,
|
"colour": 224,
|
||||||
"tooltip": "Create smart triggers based on numerical sensor data and thresholds. Perfect for temperature alerts ('notify when above 80°F'), battery monitoring ('warn when below 20%'), humidity control ('turn on fan when over 60%'), or any sensor-based automation that depends on numerical comparisons.",
|
"tooltip": "Numerically compare the new Feed value with another number.",
|
||||||
"output": "matcher",
|
"output": "matcher",
|
||||||
"message0": "%1 %2",
|
"message0": "%1 %2",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -2973,7 +2973,7 @@ exports[`Block Snapshots > Blockly JSON > matcher_text_compare 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "matcher_text_compare",
|
"type": "matcher_text_compare",
|
||||||
"colour": 180,
|
"colour": 180,
|
||||||
"tooltip": "Compare text-based feed data using smart text matching. Perfect for triggers based on status messages ('door opened', 'motion detected'), device states ('online', 'offline'), or any text-based sensor data. Works with exact matches, exclusions, or partial text detection within longer messages.",
|
"tooltip": "Compare the new feed value with text for equality, inequality, or inclusion.",
|
||||||
"output": "matcher",
|
"output": "matcher",
|
||||||
"message0": "%1 %2",
|
"message0": "%1 %2",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -3130,7 +3130,7 @@ exports[`Block Snapshots > Blockly JSON > on_schedule 1`] = `
|
||||||
"inputsInline": false,
|
"inputsInline": false,
|
||||||
"type": "on_schedule",
|
"type": "on_schedule",
|
||||||
"colour": 30,
|
"colour": 30,
|
||||||
"tooltip": "Create powerful time-based automation that runs your Actions on a schedule - from simple daily reminders to complex patterns like 'every 15 minutes during weekdays' or 'first Monday of each quarter'. Works like a smart alarm clock for your IoT devices, automatically triggering actions without any manual intervention. Perfect for turning lights on/off, sending regular reports, or controlling devices based on time patterns.",
|
"tooltip": "A schedule to run the action, from every minute to once a year.",
|
||||||
"nextStatement": "trigger",
|
"nextStatement": "trigger",
|
||||||
"previousStatement": "trigger",
|
"previousStatement": "trigger",
|
||||||
"message0": "Schedule %1",
|
"message0": "Schedule %1",
|
||||||
|
|
@ -3886,7 +3886,7 @@ exports[`Block Snapshots > Blockly JSON > text_compare 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "text_compare",
|
"type": "text_compare",
|
||||||
"colour": 180,
|
"colour": 180,
|
||||||
"tooltip": "Compare any two pieces of text or data to build conditional logic in your Actions. Perfect for creating if/then statements like 'if device status equals online', 'if user name is not guest', or 'if error message contains timeout'. Works with feed values, variables, user input, or any text-based data.",
|
"tooltip": "Compare two chunks of text for equality, inequality, or inclusion.",
|
||||||
"output": "expression",
|
"output": "expression",
|
||||||
"message0": "%1",
|
"message0": "%1",
|
||||||
"args0": [
|
"args0": [
|
||||||
|
|
@ -4109,7 +4109,7 @@ exports[`Block Snapshots > Blockly JSON > when_data 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "when_data",
|
"type": "when_data",
|
||||||
"colour": 30,
|
"colour": 30,
|
||||||
"tooltip": "The simplest trigger - runs your Action every single time ANY new data arrives at a feed, regardless of what the value is. Perfect for logging all activity ('record every sensor reading'), acknowledging data receipt ('send confirmation for every message'), or triggering workflows that need to process all incoming data. No conditions, no filtering - just pure data arrival detection.",
|
"tooltip": "Run this action when a Feed receives a new data point.",
|
||||||
"nextStatement": "trigger",
|
"nextStatement": "trigger",
|
||||||
"previousStatement": "trigger",
|
"previousStatement": "trigger",
|
||||||
"message0": "When %1 gets any data %2",
|
"message0": "When %1 gets any data %2",
|
||||||
|
|
@ -4142,7 +4142,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "when_data_matching",
|
"type": "when_data_matching",
|
||||||
"colour": 30,
|
"colour": 30,
|
||||||
"tooltip": "The most common trigger type - runs your Action immediately whenever new data arrives at a feed that meets your specified condition. Perfect for real-time responses like 'send alert when temperature exceeds 85°F', 'turn on lights when motion detected', or 'notify me when battery drops below 20%'. This trigger fires every single time the condition is met.",
|
"tooltip": "Run this Action when the specified Feed receives data that matches the specified condition.",
|
||||||
"nextStatement": "trigger",
|
"nextStatement": "trigger",
|
||||||
"previousStatement": "trigger",
|
"previousStatement": "trigger",
|
||||||
"message0": "When %1 gets data matching: %2",
|
"message0": "When %1 gets data matching: %2",
|
||||||
|
|
@ -4177,7 +4177,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching_state 1`] = `
|
||||||
"inputsInline": true,
|
"inputsInline": true,
|
||||||
"type": "when_data_matching_state",
|
"type": "when_data_matching_state",
|
||||||
"colour": 30,
|
"colour": 30,
|
||||||
"tooltip": "Advanced trigger that watches for changes in how your feed data matches a condition over time. Unlike basic triggers that just check if data equals a value, this compares the current data point with the previous one to detect when conditions START being true, STOP being true, or CONTINUE being true. Perfect for detecting state changes like 'temperature just went above 80°' or 'door just closed after being open'.",
|
"tooltip": "Run this Action when the specified Feed receives a data point that compares to its previous data point in the specified way.",
|
||||||
"nextStatement": "trigger",
|
"nextStatement": "trigger",
|
||||||
"previousStatement": "trigger",
|
"previousStatement": "trigger",
|
||||||
"message0": "When %1 gets data that %2 matching %3",
|
"message0": "When %1 gets data that %2 matching %3",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue