Merge branch 'main' into air-quality
This commit is contained in:
commit
9a3075da9c
95 changed files with 3614 additions and 1331 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,7 +2,9 @@
|
|||
.ds_store
|
||||
|
||||
# Blockly exports
|
||||
tmp
|
||||
export
|
||||
docs/block_images
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { singleLineTemplate, multilineLineTemplate } from "#app/blocks/shadows.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "action_email",
|
||||
bytecodeKey: "emailAction",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "action_log",
|
||||
bytecodeKey: "logAction",
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
// deprecated: use feeds/set_value
|
||||
export default {
|
||||
type: "action_publish",
|
||||
bytecodeKey: "publishAction",
|
||||
name: "Publish to Feed",
|
||||
colour: "0",
|
||||
description: "Sends the given value to the specified Feed.",
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
template: `
|
||||
📈 Publish |CENTER
|
||||
...value: %VALUE
|
||||
...to: %FEED
|
||||
`,
|
||||
|
||||
inputs: {
|
||||
VALUE: {
|
||||
description: "The value to write to the Feed.",
|
||||
check: "expression",
|
||||
shadow: 'io_text'
|
||||
},
|
||||
|
||||
FEED: {
|
||||
description: "The Feed to write to.",
|
||||
check: "expression",
|
||||
shadow: 'feed_selector'
|
||||
},
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
publishAction: {
|
||||
feed: JSON.parse(generator.valueToCode(block, 'FEED', 0) || null),
|
||||
value: JSON.parse(generator.valueToCode(block, 'VALUE', 0) || null)
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.publishAction
|
||||
|
||||
// migrating to a new block
|
||||
return {
|
||||
type: "feed_set_value",
|
||||
fields: {
|
||||
FEED_KEY: payload.feed.feed.key,
|
||||
},
|
||||
inputs: {
|
||||
VALUE: helpers.expressionToBlock(payload.value, { shadow: 'io_text' }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { multilineLineTemplate } from "#app/blocks/shadows.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "action_sms",
|
||||
bytecodeKey: "smsAction",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { multilineLineTemplate } from "#app/blocks/shadows.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "action_webhook",
|
||||
bytecodeKey: "webhookAction",
|
||||
|
|
|
|||
|
|
@ -1,110 +1,124 @@
|
|||
import mutator from './if/mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_controls_if',
|
||||
bytecodeKey: "conditional",
|
||||
name: "Conditional",
|
||||
description: "Execute different block diagrams based on the outcome of conditional checks.",
|
||||
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.",
|
||||
colour: 60,
|
||||
|
||||
// "if - the first value to check for truthiness",
|
||||
// "do - the commands to execute if the first check was true",
|
||||
// "else if - (optional, repeating) an extra value to check for truthiness",
|
||||
// "do - the commands to execute if the previous check was true",
|
||||
// "else - (optional) the commands to execute if no checks were true",
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "expression",
|
||||
next: 'expression'
|
||||
},
|
||||
|
||||
mutator,
|
||||
|
||||
template: `
|
||||
if %IF0
|
||||
do %THEN0
|
||||
else if %ELSE_IF_LABEL
|
||||
else %ELSE_LABEL
|
||||
`,
|
||||
|
||||
// TODO: open a way to send raw documentation for the inputs section
|
||||
// the conditional block has a lot of dynamic behavior that is harder to
|
||||
// document plainly alongside the data definitions
|
||||
|
||||
inputs: {
|
||||
IF0: {
|
||||
description: "Runs the given block tree and checks whether it resolve true or false. If true, executes the 'do' branch, otherwise moves onto the next if (if present), or the final else (if present.)",
|
||||
check: "expression",
|
||||
shadow: 'io_logic_boolean'
|
||||
},
|
||||
|
||||
THEN0: {
|
||||
description: "The block diagram to execute when the preceding 'if' clause resolves to true",
|
||||
check: "expression",
|
||||
type: 'statement',
|
||||
shadow: {
|
||||
type:'action_log',
|
||||
inputs: {
|
||||
EXPRESSION: {
|
||||
shadow: {
|
||||
type: 'io_text',
|
||||
fields: {
|
||||
TEXT: 'conditional was true!'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ELSE_IF_LABEL: {
|
||||
type: 'label',
|
||||
},
|
||||
|
||||
ELSE_LABEL: {
|
||||
type: 'label',
|
||||
}
|
||||
},
|
||||
docOverrides: {
|
||||
inputs: `
|
||||
### \`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).
|
||||
|
||||
**Examples:**
|
||||
- \`temperature > 80\` - Check if it's hot
|
||||
- \`motion detected AND time after sunset\` - Security logic
|
||||
- \`battery level < 15%\` - Low power warning
|
||||
|
||||
### \`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.
|
||||
|
||||
**Examples:**
|
||||
- Send alert email + turn on cooling fan
|
||||
- Log warning message + publish backup data
|
||||
- Turn on lights + send notification
|
||||
|
||||
### \`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.
|
||||
|
||||
**Examples:**
|
||||
- \`else if temperature < 60\` → turn on heater
|
||||
- \`else if battery < 50%\` → reduce power consumption
|
||||
- \`else if motion detected\` → different security response
|
||||
|
||||
### \`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.
|
||||
|
||||
**Examples:**
|
||||
- Turn off all climate control (temperature is in normal range)
|
||||
- Send "all systems normal" status update
|
||||
- Resume regular power consumption mode
|
||||
`
|
||||
},
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const payload = {
|
||||
conditional: {}
|
||||
}
|
||||
|
||||
let index = 0
|
||||
while(block.getInput(`IF${index}`)) {
|
||||
const
|
||||
ifClause = generator.valueToCode(block, `IF${index}`, 0) || 'null',
|
||||
thenClause = generator.statementToCode(block, `THEN${index}`) || ''
|
||||
|
||||
payload.conditional[`if${index}`] = JSON.parse(ifClause)
|
||||
payload.conditional[`then${index}`] = JSON.parse(`[ ${thenClause} ]`)
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
if(block.getInput('ELSE')) {
|
||||
const elseClause = generator.statementToCode(block, 'ELSE') || ''
|
||||
|
||||
payload.conditional.else = JSON.parse(`[${ elseClause }]`)
|
||||
}
|
||||
|
||||
return JSON.stringify(payload)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (bytecode, helpers) => {
|
||||
const payload = bytecode.conditional
|
||||
|
||||
if(!payload) {
|
||||
throw new Error("No data for io_controls_if regenerator")
|
||||
}
|
||||
|
||||
const inputs = {}
|
||||
|
||||
let index = 0
|
||||
while(payload[`if${index}`] || payload[`then${index}`]) {
|
||||
inputs[`IF${index}`] = helpers.expressionToBlock(payload[`if${index}`], { shadow: 'io_logic_boolean' })
|
||||
inputs[`THEN${index}`] = helpers.arrayToStatements(payload[`then${index}`])
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
if(payload.else) {
|
||||
inputs.ELSE = helpers.arrayToStatements(payload.else)
|
||||
}
|
||||
|
||||
return {
|
||||
type: "io_controls_if",
|
||||
inputs,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "feed_get_value",
|
||||
bytecodeKey: "getFeedValue",
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
// deprecated: use feeds/get_value
|
||||
export default {
|
||||
type: "feed_selector",
|
||||
bytecodeKey: "feed",
|
||||
name: "Feed",
|
||||
colour: 300,
|
||||
description: "The last value of this feed or component, always a String",
|
||||
|
||||
mixins: ['replaceDropdownOptions'],
|
||||
extensions: ['populateFeedDropdown'],
|
||||
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: "Feed: %FEED_KEY",
|
||||
|
||||
fields: {
|
||||
FEED_KEY: {
|
||||
description: "A listing of the User's Feeds to select from.",
|
||||
options: [
|
||||
[ "Loading Feeds...", "" ],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
key = block.getFieldValue('FEED_KEY'),
|
||||
payload = JSON.stringify({
|
||||
feed: { key }
|
||||
})
|
||||
|
||||
return [ payload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: blockObject => {
|
||||
const payload = blockObject.feed
|
||||
|
||||
// migrating to a new block
|
||||
return {
|
||||
type: "feed_get_value",
|
||||
fields: {
|
||||
FEED_KEY: payload.key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "feed_set_value",
|
||||
bytecodeKey: "setFeedValue",
|
||||
|
|
|
|||
|
|
@ -1,29 +1,26 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_logic_boolean',
|
||||
name: "Boolean",
|
||||
colour: 60,
|
||||
description: "A true or false value.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: [ "expression", "boolean" ],
|
||||
},
|
||||
|
||||
template: "%BOOL",
|
||||
|
||||
fields: {
|
||||
BOOL: {
|
||||
description: "Choose the boolean state you want to use:",
|
||||
options: [
|
||||
['true', 'TRUE'],
|
||||
['false', 'FALSE'],
|
||||
['true', 'TRUE', "Represents 'on', 'yes', 'enabled', or 'active' state. Use for turning on devices, enabling features, or setting positive conditions."],
|
||||
['false', 'FALSE', "Represents 'off', 'no', 'disabled', or 'inactive' state. Use for turning off devices, disabling features, or setting negative conditions."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const bool = block.getFieldValue('BOOL') === 'TRUE'
|
||||
|
||||
return [ JSON.stringify(bool), 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_logic_negate',
|
||||
bytecodeKey: "negate",
|
||||
name: "Negate",
|
||||
colour: 60,
|
||||
description: "Swaps a truthy value to `false`, or a falsy value to `true`.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: "not %EXPRESSION",
|
||||
|
||||
inputs: {
|
||||
EXPRESSION: {
|
||||
description: "Block diagram that will be resolved, then have its truthiness flipped.",
|
||||
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).",
|
||||
check: "expression",
|
||||
shadow: 'io_logic_boolean'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
|
|
@ -29,15 +26,12 @@ export default {
|
|||
target: JSON.parse(operand)
|
||||
}
|
||||
}
|
||||
|
||||
return [ JSON.stringify(payload), 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.negate
|
||||
|
||||
return {
|
||||
type: 'io_logic_negate',
|
||||
inputs: {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,43 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_logic_operation',
|
||||
bytecodeKey: "logic",
|
||||
name: "Logic Operation",
|
||||
inputsInline: true,
|
||||
colour: 60,
|
||||
description: "Perform the specifed boolean logic operation on two operands.",
|
||||
|
||||
description: "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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: `%A %OP %B`,
|
||||
|
||||
inputs: {
|
||||
A: {
|
||||
description: "A block diagram that will be resolved to a truthy/falsy value",
|
||||
description: "The first condition to evaluate (left side). Connect comparison blocks, sensor checks, or any logic that results in true/false. Examples: 'temperature > 80', 'door equals open', or 'battery < 20%'.",
|
||||
check: "expression",
|
||||
shadow: 'io_logic_boolean'
|
||||
},
|
||||
|
||||
B: {
|
||||
description: "A block diagram that will be resolved to a truthy/falsy value",
|
||||
description: "The second condition to evaluate (right side). Connect another comparison, sensor check, or boolean logic. Examples: 'humidity > 60%', 'motion detected', or 'time between 9 AM and 5 PM'.",
|
||||
check: "expression",
|
||||
shadow: 'io_logic_boolean'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
description: "Select the logic operation to perform on the inputs:",
|
||||
description: "Choose how to combine your two conditions:",
|
||||
options: [
|
||||
['and', 'AND', "Resolve `true` if both operands are true, otherwise `false`."],
|
||||
['or', 'OR', "Resolve `true` if either or both operands are true, otherwise `false`."],
|
||||
['and', 'AND', "Both conditions must be true: Returns true only when BOTH inputs are true (e.g., 'temperature > 80 AND humidity < 30' for hot and dry conditions requiring both criteria simultaneously)."],
|
||||
['or', 'OR', "Either condition can be true: Returns true when AT LEAST ONE input is true (e.g., 'motion detected OR door opened' triggers on any activity, 'battery < 10% OR offline > 1 hour' for multiple alert conditions)."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
operator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
logic: {
|
||||
left: JSON.parse(leftExp),
|
||||
|
|
@ -51,11 +45,9 @@ export default {
|
|||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -67,7 +59,6 @@ export default {
|
|||
A: helpers.expressionToBlock(left, { shadow: 'io_logic_boolean' }),
|
||||
B: helpers.expressionToBlock(right, { shadow: 'io_logic_boolean' }),
|
||||
}
|
||||
|
||||
return { type: 'io_logic_operation', fields, inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
disabled: true,
|
||||
type: 'io_logic_ternary',
|
||||
name: "Ternary",
|
||||
colour: 60,
|
||||
description: "Simpler conditional, inline if/then/else",
|
||||
|
||||
template: `
|
||||
if %IF
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
export default {
|
||||
type: 'matcher_boolean_operation',
|
||||
bytecodeKey: "matcherBooleanOperation",
|
||||
name: "Compare Matcher",
|
||||
colour: 60,
|
||||
inputsInline: true,
|
||||
description: "Perform a logic operation between the triggering Feed value and a block diagram.",
|
||||
|
||||
connections: { mode: 'value', output: 'matcher' },
|
||||
|
||||
template: `is true %OP %B`,
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
options: [
|
||||
['and', 'AND'],
|
||||
['or', 'OR'],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
inputs: {
|
||||
B: {
|
||||
check: "expression",
|
||||
shadow: 'io_logic_boolean'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
operator = block.getFieldValue('OP'),
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
matcherBooleanOperation: {
|
||||
comparator: operator?.toLowerCase() || null,
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ comparator, right } = blockObject.matcherBooleanOperation,
|
||||
fields = {
|
||||
OP: comparator?.toUpperCase()
|
||||
},
|
||||
inputs = {
|
||||
B: helpers.expressionToBlock(right, { shadow: 'io_logic_boolean' }),
|
||||
}
|
||||
|
||||
return { type: 'matcher_boolean_operation', fields, inputs }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +1,47 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'matcher_compare',
|
||||
bytecodeKey: "matcherCompare",
|
||||
name: "Compare Numbers Matcher",
|
||||
colour: 224,
|
||||
inputsInline: true,
|
||||
description: "Numerically compare the new Feed value with another number.",
|
||||
|
||||
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.",
|
||||
connections: { mode: 'value', output: 'matcher' },
|
||||
|
||||
template: "%OP %B",
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
description: "Select a comparison to perform",
|
||||
description: "Choose how to compare your feed's numerical data:",
|
||||
options: [
|
||||
['=', 'EQ', "True if the two numbers are equal"],
|
||||
['\u2260', 'NEQ', "True if the two numbers are not equal"],
|
||||
['\u200F<', 'LT', "True if the Feed value is less than number B"],
|
||||
['\u200F\u2264', 'LTE', "True if the Feed value is less than or equal to number B"],
|
||||
['\u200F>', 'GT', "True if the Feed value is greater than number B"],
|
||||
['\u200F\u2265', 'GTE', "True if the Feed value is greater than or equal to number B"],
|
||||
['=', '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."],
|
||||
['\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."],
|
||||
['\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\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>', '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\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)."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
inputs: {
|
||||
B: {
|
||||
description: "The value to compare with the Feed value.",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_math_number'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
comparator = block.getFieldValue('OP'),
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
matcherCompare: {
|
||||
comparator: comparator?.toLowerCase() || null,
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -59,7 +52,6 @@ export default {
|
|||
inputs = {
|
||||
B: helpers.expressionToBlock(right, { shadow: 'io_math_number' }),
|
||||
}
|
||||
|
||||
return { type: 'matcher_compare', fields, inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,44 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'matcher_text_compare',
|
||||
bytecodeKey: "matcherTextCompare",
|
||||
name: "Compare Text Matcher",
|
||||
colour: 180,
|
||||
inputsInline: true,
|
||||
description: "Compare the new feed value with text for equality, inequality, or inclusion.",
|
||||
|
||||
description: "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.",
|
||||
connections: { mode: 'value', output: 'matcher' },
|
||||
|
||||
template: "%OP %B",
|
||||
|
||||
inputs: {
|
||||
B: {
|
||||
description: "The string to compare with the Feed value.",
|
||||
description: "The text to compare against your feed data. Examples: 'open' to detect door status, 'motion' for PIR sensors, 'online' for device connectivity, or any specific word/phrase you're monitoring for.",
|
||||
check: "expression",
|
||||
shadow: 'io_text'
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
description: "Select what kind of comparison to do:",
|
||||
description: "Choose how to compare the incoming feed data with your text:",
|
||||
options: [
|
||||
['=', 'EQ', "Returns true if the Feed value and text are the same."],
|
||||
['\u2260', 'NEQ', "Returns true if the Feed value and text are not the same."],
|
||||
['includes', 'INC', "Returns true if the Feed value includes the text."],
|
||||
['=', 'EQ', "Exact match: Feed value must be exactly the same as your text (e.g., feed='online' matches text='online', but 'ONLINE' or 'device online' would not match)."],
|
||||
['\u2260', 'NEQ', "Not equal: Feed value must be different from your text (e.g., useful for 'not offline' conditions or excluding specific status messages)."],
|
||||
['includes', 'INC', "Contains: Feed value includes your text anywhere within it (e.g., feed='motion detected at 3pm' would match text='motion', perfect for parsing longer status messages)."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
comparator = block.getFieldValue('OP'),
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
matcherTextCompare: {
|
||||
comparator: comparator?.toLowerCase() || null,
|
||||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -56,7 +49,6 @@ export default {
|
|||
inputs = {
|
||||
B: helpers.expressionToBlock(right, { shadow: "io_text" }),
|
||||
}
|
||||
|
||||
return { type: 'matcher_text_compare', fields, inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,40 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_math_arithmetic',
|
||||
bytecodeKey: "arithmetic",
|
||||
name: "Arithmetic",
|
||||
colour: 120,
|
||||
inputsInline: true,
|
||||
description: "Perform the specified arithmetic operation on two specified operands.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: `%A %OP %B`,
|
||||
|
||||
inputs: {
|
||||
A: {
|
||||
description: "The left side of the operation. Will be coerced to a number",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_math_number'
|
||||
},
|
||||
|
||||
B: {
|
||||
description: "The right side of the operation. Will be coerced to a number",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_math_number'
|
||||
},
|
||||
},
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
description: "The mathematical operation to perform.",
|
||||
description: "Choose the mathematical operation to perform:",
|
||||
options: [
|
||||
['+', 'ADD', "add two numbers"],
|
||||
['-', 'MINUS', "subtract number B from number A"],
|
||||
['x', 'MULTIPLY', "multiply two numbers"],
|
||||
['/', 'DIVIDE', "divide number A by number B"],
|
||||
['^', 'POWER', "raise number A to the power of number B"],
|
||||
['+', 'ADD', "Addition: Combine two numbers (e.g., 25 + 5 = 30). For totaling values, adding offsets, or combining multiple sensor readings into sums."],
|
||||
['-', 'MINUS', "Subtraction: Remove B from A (e.g., 30 - 5 = 25). For calculating differences, finding deltas between readings, or subtracting baseline values."],
|
||||
['x', 'MULTIPLY', "Multiplication: A times B (e.g., 6 x 4 = 24). For unit conversions, scaling values, calculating areas/volumes, or applying multiplication factors."],
|
||||
['/', 'DIVIDE', "Division: A divided by B (e.g., 20 ÷ 4 = 5). For calculating averages, ratios, percentages, or converting between different unit scales."],
|
||||
['^', 'POWER', "Exponentiation: A raised to the power of B (e.g., 2^3 = 8). For advanced calculations, exponential growth models, or complex mathematical formulas."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
|
|
@ -53,7 +48,6 @@ export default {
|
|||
operator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
arithmetic: {
|
||||
left: JSON.parse(leftExp),
|
||||
|
|
@ -63,11 +57,9 @@ export default {
|
|||
right: JSON.parse(rightExp)
|
||||
}
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -86,7 +78,6 @@ export default {
|
|||
A: helpers.expressionToBlock(payload.left, { shadow: 'io_math_number' }),
|
||||
B: helpers.expressionToBlock(payload.right, { shadow: 'io_math_number' }),
|
||||
}
|
||||
|
||||
return { type: 'io_math_arithmetic', fields, inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_logic_compare',
|
||||
bytecodeKey: "",
|
||||
|
|
@ -5,50 +6,43 @@ export default {
|
|||
colour: 120,
|
||||
inputsInline: true,
|
||||
primaryCategory: "Math",
|
||||
description: "Numerically compare two given values using the selected math operation.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: `%A %OP %B`,
|
||||
|
||||
inputs: {
|
||||
A: {
|
||||
description: "The left side of the comparison. Will be coerced to a number",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_math_number'
|
||||
},
|
||||
|
||||
B: {
|
||||
description: "The right side of the comparison. Will be coerced to a number",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_math_number'
|
||||
},
|
||||
},
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
description: "The mathematical comparison to use.",
|
||||
description: "Choose the mathematical relationship to test between your two numbers:",
|
||||
options: [
|
||||
['=', 'EQ', "True if the two numbers are equal"],
|
||||
['\u2260', 'NEQ', "True if the two numbers are not equal"],
|
||||
['\u200F<', 'LT', "True if number A is less than number B"],
|
||||
['\u200F\u2264', 'LTE', "True if number A is less than or equal to number B"],
|
||||
['\u200F>', 'GT', "True if number A is greater than number B"],
|
||||
['\u200F\u2265', 'GTE', "True if number A is greater than or equal to number B"],
|
||||
['=', '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)."],
|
||||
['\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)."],
|
||||
['\u200F<', 'LT', "Less than: True when first number is smaller (e.g., temperature < comfort threshold, battery < critical level for alerts)."],
|
||||
['\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>', 'GT', "Greater than: True when first number is larger (e.g., pressure > storm threshold, light level > daylight minimum for automation)."],
|
||||
['\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)."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
comparator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || 'null',
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || 'null',
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
compare: {
|
||||
left: JSON.parse(leftExp),
|
||||
|
|
@ -56,11 +50,9 @@ export default {
|
|||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -72,7 +64,6 @@ export default {
|
|||
A: helpers.expressionToBlock(left, { shadow: 'io_math_number' }),
|
||||
B: helpers.expressionToBlock(right, { shadow: 'io_math_number' }),
|
||||
}
|
||||
|
||||
return { type: 'io_logic_compare', fields, inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,26 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "io_math_constrain",
|
||||
bytecodeKey: "constrain",
|
||||
name: "Constrain",
|
||||
colour: 120,
|
||||
description: "Constrain a given number to fall within a given range.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "number",
|
||||
output: [ "expression", "number" ],
|
||||
},
|
||||
|
||||
template: `
|
||||
Constrain %VALUE
|
||||
to %RANGE
|
||||
`,
|
||||
|
||||
inputs: {
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: "io_math_number"
|
||||
},
|
||||
|
||||
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.",
|
||||
check: 'range',
|
||||
shadow: {
|
||||
type: "math_range",
|
||||
|
|
@ -38,18 +37,15 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
value = JSON.parse(generator.valueToCode(block, 'VALUE', 0)),
|
||||
range = JSON.parse(generator.valueToCode(block, 'RANGE', 0)),
|
||||
payload = { constrain: { value, range } }
|
||||
|
||||
return [ JSON.stringify(payload), 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -72,7 +68,6 @@ export default {
|
|||
}
|
||||
}),
|
||||
}
|
||||
|
||||
return { type: 'io_math_constrain', inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "math_map",
|
||||
bytecodeKey: "mapValue",
|
||||
name: "Map",
|
||||
colour: 120,
|
||||
description: "Scale a value from one range of numbers to another",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "number",
|
||||
output: [ "expression", "number" ],
|
||||
},
|
||||
|
||||
template: `
|
||||
Map
|
||||
Value: %VALUE
|
||||
From: %FROM_RANGE
|
||||
To: %TO_RANGE
|
||||
`,
|
||||
|
||||
inputs: {
|
||||
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.",
|
||||
check: "expression",
|
||||
bytecodeProperty: "value",
|
||||
shadow: 'io_math_number'
|
||||
},
|
||||
|
||||
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.",
|
||||
check: 'range',
|
||||
bytecodeProperty: "from",
|
||||
shadow: {
|
||||
|
|
@ -41,8 +40,8 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
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.",
|
||||
check: 'range',
|
||||
bytecodeProperty: "to",
|
||||
shadow: {
|
||||
|
|
@ -60,7 +59,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
|
|
@ -68,11 +66,9 @@ export default {
|
|||
from = JSON.parse(generator.valueToCode(block, 'FROM_RANGE', 0)),
|
||||
to = JSON.parse(generator.valueToCode(block, 'TO_RANGE', 0)),
|
||||
payload = { mapValue: { value, from, to }}
|
||||
|
||||
return [ JSON.stringify(payload), 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -110,7 +106,6 @@ export default {
|
|||
}
|
||||
}),
|
||||
}
|
||||
|
||||
return { type: 'math_map', inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,34 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_math_number',
|
||||
name: "Number",
|
||||
colour: 120,
|
||||
|
||||
description: "A numeric value, whole or decimal.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: [ "expression", "number" ],
|
||||
},
|
||||
|
||||
extensions: {
|
||||
validateNumbers: ({ block }) => {
|
||||
const numField = block.getField("NUM")
|
||||
|
||||
if(!numField) { throw new Error("NUM field missing on io_math_number?") }
|
||||
numField.setValidator(newValue => {
|
||||
const parsed = Number(newValue)
|
||||
|
||||
if(!parsed && parsed !== 0) {
|
||||
return null // failed to parse, signal validation failure
|
||||
|
||||
} else {
|
||||
return parsed// parsed fine, use the result
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
template: " %NUM",
|
||||
|
||||
fields: {
|
||||
NUM: { text: '0' }
|
||||
NUM: {
|
||||
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: {
|
||||
json: block => {
|
||||
return [Number(block.getFieldValue('NUM')) || '0', 0]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "math_range",
|
||||
bytecodeKey: "range",
|
||||
|
|
|
|||
|
|
@ -1,49 +1,43 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "io_math_round",
|
||||
bytecodeKey: "round",
|
||||
name: "Round/Floor/Ceiling",
|
||||
color: 120,
|
||||
description: "Round a value to the nearest whole number via round, floor, or ceiling functions",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
inputs: {
|
||||
VALUE: {
|
||||
description: "A value you'd like to round to a whole number. Will be coerced to a number.",
|
||||
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.",
|
||||
bytecodeProperty: "value",
|
||||
check: "expression",
|
||||
shadow: "io_math_number"
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
OPERATION: {
|
||||
description: "Select which rounding operation to perform on the input:",
|
||||
description: "Choose how to convert your decimal to a whole number:",
|
||||
options: [
|
||||
["Round", "round", "if .5 or higher: round up; otherwise round down"],
|
||||
["Floor", "floor", "rounds down"],
|
||||
["Ceiling", "ceiling", "rounds up"],
|
||||
["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."],
|
||||
["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."],
|
||||
["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."],
|
||||
],
|
||||
bytecodeProperty: "operation",
|
||||
}
|
||||
},
|
||||
|
||||
template: "%OPERATION %VALUE",
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
value = JSON.parse(generator.valueToCode(block, 'VALUE', 0)),
|
||||
operation = block.getFieldValue('OPERATION'),
|
||||
payload = { round: { value, operation } }
|
||||
|
||||
return [ JSON.stringify(payload), 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -54,7 +48,6 @@ export default {
|
|||
fields = {
|
||||
OPERATION: operation,
|
||||
}
|
||||
|
||||
return { type: 'io_math_round', inputs, fields }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import weatherMixin from "./weather_mixin.js"
|
||||
|
||||
|
||||
const
|
||||
{ keyToLabel, HELP_TEXT_BY_PROP: propText } = weatherMixin,
|
||||
propLines = (prefix, props) =>
|
||||
props.map(prop => `${prefix}- \`${keyToLabel(prop)}\`: ${propText[prop].description}`).join("")
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "weather",
|
||||
bytecodeKey: "weather",
|
||||
|
|
@ -30,6 +36,7 @@ export default {
|
|||
weatherLocationOptions.unshift([ "Select Location", "" ])
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
block.replaceDropdownOptions("POWER_UP_ID", weatherLocationOptions)
|
||||
|
||||
// skip the rest if we're in the toolbox
|
||||
|
|
@ -40,16 +47,16 @@ export default {
|
|||
// nope out for insertion markers
|
||||
if(block.isInsertionMarker()) { return }
|
||||
|
||||
// auto-disable block, if necessary
|
||||
// @ts-ignore auto-disable block, if necessary
|
||||
block.setEnabledByLocation()
|
||||
|
||||
// react to incoming forecast data
|
||||
const unobserve = observeData('currentWeatherByLocation', (newData = {}) => {
|
||||
// if this block is disposed, clean up this listener
|
||||
if (block.isDisposed()) { unobserve(); return }
|
||||
// update the reference to the injected/updated extension data
|
||||
// @ts-ignore update the reference to the injected/updated extension data
|
||||
block.currentWeatherByLocation = newData
|
||||
// re-run the things that use the data
|
||||
// @ts-ignore re-run the things that use the data
|
||||
block.refreshPropertyOptions({})
|
||||
})
|
||||
}, 1)
|
||||
|
|
@ -66,14 +73,12 @@ export default {
|
|||
|
||||
fields: {
|
||||
POWER_UP_ID: {
|
||||
description: "Select a location from those defined by the Weather Power-Up",
|
||||
options: [
|
||||
[ "Loading locations...", "" ],
|
||||
]
|
||||
},
|
||||
|
||||
WEATHER_TIME: {
|
||||
description: "Select which kind of forecast to query",
|
||||
options: [
|
||||
[ "Now", "current" ],
|
||||
[ "In 5 minutes", "forecast_minutes_5" ],
|
||||
|
|
@ -98,7 +103,6 @@ export default {
|
|||
},
|
||||
|
||||
WEATHER_PROPERTY: {
|
||||
description: "Select which metric of the forecast to use.",
|
||||
label: ""
|
||||
},
|
||||
|
||||
|
|
@ -107,6 +111,36 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
docOverrides: {
|
||||
fields: `
|
||||
### \`Location\`
|
||||
The list of weather locations defined in the Weather Power-Up. Select
|
||||
the location you would like weather information for.
|
||||
|
||||
### \`Forecast\` and \`Metric\`
|
||||
A list a weather forecasts to choose from. The weather metrics available
|
||||
are different based on the chosen forecast.
|
||||
|
||||
:::details \`Now\` Metrics` + propLines(`
|
||||
`, weatherMixin.CURRENT_PROPS) + `
|
||||
:::
|
||||
|
||||
:::details \`In X minutes\` Metrics` + propLines(`
|
||||
`, weatherMixin.MINUTE_PROPS) + `
|
||||
:::
|
||||
|
||||
:::details \`In X hours\` Metrics` + propLines(`
|
||||
`, weatherMixin.HOUR_PROPS) + `
|
||||
:::
|
||||
|
||||
:::details \`In X days\` Metrics
|
||||
**Some daily metrics can be narrowed to just the daytime or overnight portions.**
|
||||
` + propLines(`
|
||||
`, weatherMixin.DAY_PROPS) + `
|
||||
:::
|
||||
`
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
|
|
|
|||
|
|
@ -26,7 +26,12 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
autoDisable: true,
|
||||
|
||||
setEnabledByLocation: function() {
|
||||
// bail if this behavior has been disabled
|
||||
if(!this.autoDisable) { return }
|
||||
|
||||
// must have a location and a parent (copacetic with disableOrphans)
|
||||
if(this.getFieldValue("POWER_UP_ID") === "" || !this.getParent()) {
|
||||
this.disabled || this.setEnabled(false)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "delay_days",
|
||||
name: "Delay Days",
|
||||
|
|
@ -12,8 +13,6 @@ export default {
|
|||
template: "1 day",
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
json: () => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "delay_hours",
|
||||
name: "Delay Hours",
|
||||
|
|
@ -28,8 +29,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
json: () => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "delay_minutes",
|
||||
name: "Delay Minutes",
|
||||
|
|
@ -28,8 +29,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
json: () => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "delay_none",
|
||||
name: "No Delay",
|
||||
|
|
@ -12,8 +13,6 @@ export default {
|
|||
template: "No Delay",
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
json: () => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { makeOptions } from "#app/util/fields.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "delay_seconds",
|
||||
name: "Delay Seconds",
|
||||
|
|
@ -27,8 +28,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: () => {
|
||||
|
||||
}
|
||||
json: () => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "delay_settings",
|
||||
name: "Delay Settings",
|
||||
|
|
@ -6,7 +7,6 @@ export default {
|
|||
|
||||
connections: { },
|
||||
|
||||
|
||||
template: `
|
||||
Delay Settings |CENTER
|
||||
Delay: %DELAY_PERIOD
|
||||
|
|
@ -33,9 +33,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
|
||||
return [ {}, 0 ]
|
||||
}
|
||||
json: () => [ {}, 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,37 @@
|
|||
import mutator from './action_settings/mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "action_root",
|
||||
name: "Root",
|
||||
colour: "0",
|
||||
description: "Add Triggers to determine when this Action runs.\nAdd Actions to determine what this Action does.",
|
||||
|
||||
description: "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.",
|
||||
connections: {},
|
||||
|
||||
mutator,
|
||||
|
||||
template: `
|
||||
Triggers: |LEFT
|
||||
%TRIGGERS
|
||||
|
||||
Actions: |LEFT
|
||||
%EXPRESSIONS
|
||||
\u3164
|
||||
\u00A0
|
||||
`,
|
||||
|
||||
inputs: {
|
||||
TRIGGERS: {
|
||||
description: "Connect trigger blocks here to define WHEN your Action should run. Choose from Reactive triggers (respond to feed updates), Scheduled triggers (run at specific times), or Timer triggers (delayed responses). Multiple triggers can be chained together.",
|
||||
type: 'statement',
|
||||
check: 'trigger'
|
||||
},
|
||||
|
||||
EXPRESSIONS: {
|
||||
description: "Connect action blocks here to define WHAT happens when your triggers activate. This can include sending emails, publishing values to feeds, conditional if/then logic, mathematical operations, or webhook calls. Actions execute in sequence from top to bottom.",
|
||||
type: 'statement',
|
||||
check: 'expression'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const parseStatementToCodeAsJson = statementInputName => {
|
||||
let expressions = []
|
||||
|
||||
try {
|
||||
let expressionsJson = generator.statementToCode(block, statementInputName)
|
||||
|
||||
try {
|
||||
expressions = JSON.parse(`[${expressionsJson}]`)
|
||||
} catch(e) {
|
||||
|
|
@ -50,17 +42,16 @@ export default {
|
|||
console.error(`Error calling statementToCode on root input ${statementInputName}:`)
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
return expressions
|
||||
}
|
||||
|
||||
const
|
||||
// @ts-ignore
|
||||
seconds = block.delaySeconds,
|
||||
// @ts-ignore
|
||||
mode = block.delayMode,
|
||||
delay = (seconds > 0)
|
||||
? { seconds, mode }
|
||||
: undefined
|
||||
|
||||
return JSON.stringify({
|
||||
version: "1.0.0-beta.1",
|
||||
settings: { delay },
|
||||
|
|
@ -69,11 +60,9 @@ export default {
|
|||
}, null, 2)
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const { triggers, expressions, settings } = blockObject
|
||||
|
||||
return {
|
||||
type: "action_root",
|
||||
movable: false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'text_compare',
|
||||
bytecodeKey: 'textCompare',
|
||||
|
|
@ -5,47 +6,40 @@ export default {
|
|||
colour: 180,
|
||||
inputsInline: true,
|
||||
primaryCategory: "Logic",
|
||||
description: "Compare two chunks of text for equality, inequality, or inclusion.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: `%A %OP %B`,
|
||||
|
||||
inputs: {
|
||||
A: {
|
||||
description: "The left side of the comparison. Will be coerced to a string",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_text'
|
||||
},
|
||||
|
||||
B: {
|
||||
description: "The right side of the comparison. Will be coerced to a string",
|
||||
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.",
|
||||
check: "expression",
|
||||
shadow: 'io_text'
|
||||
},
|
||||
},
|
||||
|
||||
fields: {
|
||||
OP: {
|
||||
description: "Select what kind of comparison to do:",
|
||||
description: "Choose how to compare the two text inputs:",
|
||||
options: [
|
||||
['=', 'EQ', "Returns true if the the inputs are the same."],
|
||||
['\u2260', 'NEQ', "Returns true if the inputs not the same."],
|
||||
['includes', 'INC', "Returns true if input A includes input B."],
|
||||
['=', '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)."],
|
||||
['\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)."],
|
||||
['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)."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
comparator = block.getFieldValue('OP'),
|
||||
leftExp = generator.valueToCode(block, 'A', 0) || null,
|
||||
rightExp = generator.valueToCode(block, 'B', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
textCompare: {
|
||||
left: JSON.parse(leftExp),
|
||||
|
|
@ -53,11 +47,9 @@ export default {
|
|||
right: JSON.parse(rightExp),
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -69,7 +61,6 @@ export default {
|
|||
A: helpers.expressionToBlock(left, { shadow: "io_text" }),
|
||||
B: helpers.expressionToBlock(right, { shadow: "io_text" }),
|
||||
}
|
||||
|
||||
return { type: 'text_compare', fields, inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_text_join',
|
||||
bytecodeKey: "textJoin",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
disabled: true,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'text_template',
|
||||
bytecodeKey: "textTemplate",
|
||||
|
|
@ -5,71 +6,204 @@ export default {
|
|||
colour: 180,
|
||||
inputsInline: true,
|
||||
description: `
|
||||
Render a text template.
|
||||
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>
|
||||
|
||||
::: v-pre
|
||||
Special forms surrounded by {{ curly_braces }}
|
||||
will be replaced with their current value during the action run.
|
||||
## What is a Text Template?
|
||||
|
||||
These templates use the Liquid templating language, from Shopify, and many
|
||||
helpful functions come built-in. See the
|
||||
[Liquid Documentation](https://shopify.github.io/liquid/basics/introduction/)
|
||||
to learn more.
|
||||
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.
|
||||
|
||||
### Template Variables:
|
||||
## How It Works
|
||||
|
||||
\`{{ variables.var_name }}\` - get the value of a variable you have defined
|
||||
with name 'var_name'
|
||||
This block renders (processes) a text template, replacing special placeholders with actual values.
|
||||
Anything surrounded by {{ double curly braces }} gets replaced with real data when your action runs.
|
||||
|
||||
\`{{ vars.var_name }}\` - shorthand for same as above
|
||||
For example:
|
||||
- Template: "Hello {{ user.name }}!"
|
||||
- Becomes: "Hello John!"
|
||||
|
||||
\`{{ variables['var name'] }}\` - get the value of a variable you have
|
||||
defined with name 'var name' (allows spaces in variable names
|
||||
These templates use the Liquid templating language from Shopify, which includes many helpful built-in functions.
|
||||
Learn more: [Liquid Documentation](https://shopify.github.io/liquid/basics/introduction/)
|
||||
|
||||
\`{{ vars['var name'] }}\` - shorthand for same as above
|
||||
## Template Variables Reference
|
||||
|
||||
\`{{ user.name }}\` - your user's name
|
||||
### User Information
|
||||
- \`{{ user.name }}\` - Your full name (e.g., "John Smith")
|
||||
- \`{{ user.username }}\` - Your username (e.g., "jsmith123")
|
||||
|
||||
\`{{ user.username }}\` - your user's username
|
||||
### Custom Variables
|
||||
- \`{{ variables.var_name }}\` - Get value of variable named 'var_name'
|
||||
- \`{{ vars.var_name }}\` - Shorthand version (same as above)
|
||||
- \`{{ variables['var name'] }}\` - Use brackets for names with spaces
|
||||
- \`{{ vars['my special var'] }}\` - Shorthand with spaces
|
||||
|
||||
\`{{ feeds['group_key.feed_key'].name }}\` - access a feed with key
|
||||
'group_key.feed_key' and get its name
|
||||
### Feed Data (Sensors & Devices)
|
||||
- \`{{ feeds['group_key.feed_key'].value }}\` - Current value from a feed
|
||||
- \`{{ 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
|
||||
|
||||
\`{{ feeds[...].key }}\` - ...get its key
|
||||
## Real-World IoT Examples
|
||||
|
||||
\`{{ feeds[...].value }}\` - ...get its last value
|
||||
### 🌡️ 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: {
|
||||
mode: "value",
|
||||
output: [ "expression", "string" ],
|
||||
},
|
||||
|
||||
template: "{{ %TEMPLATE",
|
||||
|
||||
inputs: {
|
||||
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",
|
||||
shadow: 'io_text_multiline'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
template = generator.valueToCode(block, 'TEMPLATE', 0) || null,
|
||||
|
||||
blockPayload = JSON.stringify({
|
||||
textTemplate: {
|
||||
template: JSON.parse(template)
|
||||
},
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
|
|
@ -77,7 +211,6 @@ export default {
|
|||
inputs = {
|
||||
TEMPLATE: helpers.expressionToBlock(template, { shadow: "io_text" })
|
||||
}
|
||||
|
||||
return { type: 'text_template', inputs }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,23 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "io_text",
|
||||
name: "Text",
|
||||
colour: 180,
|
||||
description: "A String of text",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: [ "expression", "string" ],
|
||||
},
|
||||
|
||||
template: `"%TEXT`,
|
||||
|
||||
fields: {
|
||||
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: ''
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const text = block.getFieldValue('TEXT')
|
||||
|
||||
return [ JSON.stringify(text), 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,23 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_text_multiline',
|
||||
name: "Multiline Text",
|
||||
colour: 180,
|
||||
description: "A String of longer-form text with newlines.",
|
||||
|
||||
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.",
|
||||
connections: {
|
||||
mode: "value",
|
||||
output: [ "expression", "string" ],
|
||||
},
|
||||
|
||||
template: "¶ %TEXT",
|
||||
|
||||
fields: {
|
||||
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: ''
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const text = block.getFieldValue('TEXT')
|
||||
|
||||
return [ JSON.stringify(text), 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "on_schedule",
|
||||
bytecodeKey: "onSchedule",
|
||||
name: "Schedule",
|
||||
colour: 30,
|
||||
description: "A schedule to run the action, from every minute to once a year.",
|
||||
description: "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.",
|
||||
|
||||
connections: {
|
||||
mode: "statement",
|
||||
|
|
@ -21,21 +22,25 @@ export default {
|
|||
|
||||
inputs: {
|
||||
MONTH: {
|
||||
description: "Choose which months to run the schedule. Options include 'All months' for year-round automation, specific months like 'January only' for seasonal control, or patterns like 'Every 3 months starting in March' for quarterly tasks.",
|
||||
check: "cron_month",
|
||||
block: "all_months",
|
||||
},
|
||||
|
||||
DAY: {
|
||||
description: "Select which days to trigger your schedule. You can choose specific calendar dates (1-31) for monthly events like 'every 15th', or days of the week (Monday-Sunday) for weekly patterns like 'every Tuesday and Thursday'. Cannot mix both date and weekday options.",
|
||||
check: "cron_day",
|
||||
block: "all_days"
|
||||
},
|
||||
|
||||
HOUR: {
|
||||
description: "Set the hour(s) when your Action should run, using 24-hour format (0-23). Examples: '9' for 9 AM, '14' for 2 PM, '21' for 9 PM, or 'Every 4 hours' for repeated intervals throughout the day.",
|
||||
check: "cron_hour",
|
||||
block: "all_hours"
|
||||
},
|
||||
|
||||
MINUTE: {
|
||||
description: "Specify the exact minute(s) within the hour to run your Action (0-59). Examples: '0' for on-the-hour (like 9:00), '15' for quarter-past (like 9:15), '30' for half-past, or 'Every 15 minutes' for frequent automation.",
|
||||
check: "cron_minute",
|
||||
block: {
|
||||
type: "one_minute",
|
||||
|
|
@ -68,13 +73,13 @@ export default {
|
|||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
json: (blockObject) => {
|
||||
const
|
||||
EVERY_REGEX = /^(\d{1,2})(-(\d{1,2}))?\/(\d{1,2})$/m,
|
||||
isEveryBetween = cron => EVERY_REGEX.test(cron),
|
||||
|
||||
everyBetweenToBlock = (everyBetweenCron, blockType) => {
|
||||
const [ skip1, START, skip2, END, FREQUENCY ] = everyBetweenCron.match(EVERY_REGEX)
|
||||
const [ , START, , END, FREQUENCY ] = everyBetweenCron.match(EVERY_REGEX)
|
||||
|
||||
return { block: {
|
||||
type: blockType,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from "./day_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "all_days",
|
||||
name: "All Days",
|
||||
|
|
@ -17,8 +18,6 @@ export default {
|
|||
template: "Every day",
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
json: () => [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "day_settings",
|
||||
name: "Day Settings",
|
||||
|
|
@ -16,6 +17,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
json: () => { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from "./day_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "days_of_week",
|
||||
name: "Days of the Week",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from "./day_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "every_days_between",
|
||||
name: "Every X Days",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from "./day_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "one_day",
|
||||
name: "One Day",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from "./hour_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "all_hours",
|
||||
name: "All Hours",
|
||||
|
|
@ -17,8 +18,6 @@ export default {
|
|||
template: "Every hour",
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
json: () => [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from "./hour_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "every_hours_between",
|
||||
name: "Every X Hours",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "hour_settings",
|
||||
name: "Hour Settings",
|
||||
|
|
@ -16,6 +17,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
json: () => { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from "./hour_mutator.js"
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "one_hour",
|
||||
name: "One Hour",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from './minute_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "all_minutes",
|
||||
name: "All Minutes",
|
||||
|
|
@ -17,8 +18,6 @@ export default {
|
|||
template: "Every minute",
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
json: () => [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from './minute_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "every_minutes_between",
|
||||
name: "Every X Minutes",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "minute_settings",
|
||||
name: "Minute Settings",
|
||||
|
|
@ -16,6 +17,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
json: () => { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from './minute_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "one_minute",
|
||||
name: "One Minute",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from './month_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "all_months",
|
||||
name: "All Months",
|
||||
|
|
@ -17,8 +18,6 @@ export default {
|
|||
template: "Every month",
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
return [ '*', 0 ]
|
||||
}
|
||||
json: () => [ '*', 0 ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { makeOptions } from "#app/util/fields.js"
|
|||
import mutator from './month_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "every_months_between",
|
||||
name: "Every X Months",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "month_settings",
|
||||
name: "Month Settings",
|
||||
|
|
@ -16,6 +17,6 @@ export default {
|
|||
},
|
||||
|
||||
generators: {
|
||||
json: block => { }
|
||||
json: () => { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from './month_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "one_month",
|
||||
name: "One Month",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import mutator from './month_mutator.js'
|
||||
|
||||
|
||||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'some_months',
|
||||
name: "Some Months",
|
||||
|
|
|
|||
|
|
@ -1,31 +1,27 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "when_data",
|
||||
bytecodeKey: "whenData",
|
||||
name: "Any Data",
|
||||
colour: 30,
|
||||
inputsInline: true,
|
||||
description: "Run this action when a Feed receives a new data point.",
|
||||
|
||||
description: "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.",
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "trigger",
|
||||
next: "trigger"
|
||||
},
|
||||
|
||||
mixins: ['replaceDropdownOptions'],
|
||||
extensions: ['populateFeedDropdown'],
|
||||
|
||||
template: "When %FEED_KEY gets any data |LEFT",
|
||||
|
||||
fields: {
|
||||
FEED_KEY: {
|
||||
description: "the Feed to watch for new data points",
|
||||
description: "Choose which feed to monitor for activity. Every single data point that arrives at this feed will trigger your Action - whether it's a number, text, true/false, or any other value type.",
|
||||
options: [
|
||||
[ "Loading Feeds...", ""]
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
|
|
@ -33,15 +29,12 @@ export default {
|
|||
payload = JSON.stringify({
|
||||
whenData: { feed }
|
||||
})
|
||||
|
||||
return payload
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: blockObject => {
|
||||
const payload = blockObject.whenData
|
||||
|
||||
return {
|
||||
type: "when_data",
|
||||
fields: {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "when_data_matching",
|
||||
bytecodeKey: "whenDataMatching",
|
||||
name: "Data Matching",
|
||||
colour: 30,
|
||||
inputsInline: true,
|
||||
description: "Run this Action when the specified Feed receives data that matches the specified condition.",
|
||||
|
||||
description: "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.",
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "trigger",
|
||||
next: "trigger"
|
||||
},
|
||||
|
||||
mixins: ['replaceDropdownOptions'],
|
||||
extensions: [ "populateFeedDropdown" ],
|
||||
|
||||
template: "When %FEED_KEY gets data matching: %MATCHER",
|
||||
|
||||
inputs: {
|
||||
MATCHER: {
|
||||
description: "Attach a Matcher block to compare against new Feed values",
|
||||
description: "Define the condition that incoming data must meet to trigger this Action. Examples: 'equals 1' (for button presses), 'greater than 80' (for temperature alerts), 'contains \"motion\"' (for text matching), or 'any value' (triggers on every data point regardless of value).",
|
||||
check: 'matcher',
|
||||
shadow: {
|
||||
type: 'matcher_compare',
|
||||
|
|
@ -29,16 +26,14 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
FEED_KEY: {
|
||||
description: "Select the Feed to watch for new data.",
|
||||
description: "Choose which feed to monitor for new incoming data. Every time fresh data arrives at this feed, it will be tested against your matcher condition.",
|
||||
options: [
|
||||
[ "Loading Feeds...", ""]
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
|
|
@ -49,15 +44,12 @@ export default {
|
|||
feed, matcher
|
||||
}
|
||||
})
|
||||
|
||||
return payload
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.whenDataMatching
|
||||
|
||||
return {
|
||||
type: "when_data_matching",
|
||||
fields: {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: "when_data_matching_state",
|
||||
bytecodeKey: "whenDataMatchStateChanged",
|
||||
name: "Data Match Changing",
|
||||
colour: 30,
|
||||
inputsInline: true,
|
||||
description: "Run this Action when the specified Feed receives a data point that compares to its previous data point in the specified way.",
|
||||
|
||||
description: "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'.",
|
||||
connections: {
|
||||
mode: "statement",
|
||||
output: "trigger",
|
||||
next: "trigger"
|
||||
},
|
||||
|
||||
mixins: [ 'replaceDropdownOptions' ],
|
||||
extensions: [ "populateFeedDropdown" ],
|
||||
|
||||
template: "When %FEED_KEY gets data that %MATCH_STATE matching %MATCHER",
|
||||
|
||||
inputs: {
|
||||
MATCHER: {
|
||||
description: "Attach a Matcher block to apply to both data points to make the specified comparison",
|
||||
description: "The condition to test against both the previous and current data points. For example: 'equals 1', 'greater than 80', or 'contains \"open\"'. This same condition is applied to both the old and new values to determine the state change.",
|
||||
check: 'matcher',
|
||||
shadow: {
|
||||
type: 'matcher_compare',
|
||||
|
|
@ -29,27 +26,24 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
FEED_KEY: {
|
||||
description: "Select a Feed to watch for new data",
|
||||
description: "Choose which feed to monitor for incoming data. Each new data point will be compared against the previous one using your matcher condition.",
|
||||
align: "LEFT",
|
||||
options: [
|
||||
[ "Loading Feeds...", ""]
|
||||
]
|
||||
},
|
||||
|
||||
MATCH_STATE: {
|
||||
description: "Select the kind of change to watch for:",
|
||||
description: "Select what kind of change pattern you want to detect between the previous and current data points:",
|
||||
options: [
|
||||
["starts", "starts", "the last data point DID NOT match, but this one DOES"],
|
||||
["stops", "stops", "the last data point DID match, but this one DOES NOT"],
|
||||
["keeps", "keeps", "both data points DO match"],
|
||||
["keeps not", "avoids", "both data points DO NOT match"],
|
||||
["starts", "starts", "Triggers when the condition becomes true for the first time (previous data didn't match, but new data does). Example: temperature was below 80°, now it's above 80°."],
|
||||
["stops", "stops", "Triggers when the condition stops being true (previous data matched, but new data doesn't). Example: door was open, now it's closed."],
|
||||
["keeps", "keeps", "Triggers when the condition remains true (both previous and current data match). Example: temperature stays above 80° for multiple readings."],
|
||||
["keeps not", "avoids", "Triggers when the condition remains false (both previous and current data don't match). Example: temperature stays below 80° for multiple readings."],
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
|
|
@ -61,15 +55,12 @@ export default {
|
|||
feed, matcher, state
|
||||
}
|
||||
})
|
||||
|
||||
return payload
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const { feed, matcher, state } = blockObject.whenDataMatchStateChanged
|
||||
|
||||
return {
|
||||
type: "when_data_matching_state",
|
||||
fields: {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_variables_get',
|
||||
bytecodeKey: "getVariable",
|
||||
name: "Get Variable",
|
||||
colour: 240,
|
||||
description: "Get the value previously assigned to a variable.",
|
||||
|
||||
description: "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.",
|
||||
connections: {
|
||||
mode: 'value',
|
||||
output: "expression",
|
||||
},
|
||||
|
||||
template: "Get variable %VAR",
|
||||
|
||||
fields: {
|
||||
VAR: {
|
||||
description: "Select the variable name whose value you want to retrieve. The variable must have been created and assigned a value earlier in your Action using a Set Variable block.",
|
||||
type: 'field_variable'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: block => {
|
||||
const
|
||||
|
|
@ -27,17 +25,14 @@ export default {
|
|||
name
|
||||
}
|
||||
})
|
||||
|
||||
return [ blockPayload, 0 ]
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ name } = blockObject.getVariable,
|
||||
id = helpers.registerVariable(name)
|
||||
|
||||
return {
|
||||
type: "io_variables_get",
|
||||
fields: {
|
||||
|
|
|
|||
|
|
@ -1,41 +1,40 @@
|
|||
/** @type {import('#types').BlockDefinitionRaw} */
|
||||
export default {
|
||||
type: 'io_variables_set',
|
||||
bytecodeKey: "setVariable",
|
||||
name: "Set Variable",
|
||||
inputsInline: true,
|
||||
colour: 240,
|
||||
description: "Set a variable to a value",
|
||||
|
||||
description: "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.",
|
||||
connections: {
|
||||
mode: 'statement',
|
||||
output: "expression",
|
||||
next: "expression",
|
||||
},
|
||||
|
||||
template: "Set variable %VAR = %VALUE",
|
||||
|
||||
inputs: {
|
||||
VALUE: {
|
||||
description: "The value to store in the variable. This can be text, numbers, feed values, calculation results, or data from other blocks.",
|
||||
check: "expression",
|
||||
shadow: "io_text",
|
||||
}
|
||||
},
|
||||
|
||||
fields: {
|
||||
VAR: {
|
||||
description: "Choose or create a variable name to store the value. Use descriptive names like 'temperature_reading' or 'user_count' to make your Actions easier to understand.",
|
||||
type: 'field_variable'
|
||||
}
|
||||
},
|
||||
|
||||
generators: {
|
||||
json: (block, generator) => {
|
||||
const
|
||||
variableName = block.getField('VAR').getText(),
|
||||
value = generator.valueToCode(block, 'VALUE', 0)
|
||||
|
||||
const
|
||||
// TODO: this is suspect, try valueToCode() || null, above
|
||||
defaultedValue = value
|
||||
? JSON.parse(value)
|
||||
// @ts-ignore
|
||||
: (value !== 0 && value !== null) && null,
|
||||
blockPayload = JSON.stringify({
|
||||
setVariable: {
|
||||
|
|
@ -43,17 +42,14 @@ export default {
|
|||
value: defaultedValue
|
||||
}
|
||||
})
|
||||
|
||||
return blockPayload
|
||||
}
|
||||
},
|
||||
|
||||
regenerators: {
|
||||
json: (blockObject, helpers) => {
|
||||
const
|
||||
{ name, value } = blockObject.setVariable,
|
||||
id = helpers.registerVariable(name)
|
||||
|
||||
return {
|
||||
type: "io_variables_set",
|
||||
fields: {
|
||||
|
|
|
|||
40
app/regenerators/migrations.js
Normal file
40
app/regenerators/migrations.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// These are for blocks that have been replaced with new blocks, but still
|
||||
// exist in the database. The block definition gets removed, but a new
|
||||
// regenerator is written here to catch those legacy blocks and "port" them
|
||||
// to the new block when they are loaded.
|
||||
|
||||
|
||||
export default {
|
||||
// becomes feed_set_value
|
||||
action_publish: {
|
||||
/** @type {import('#types').BlockRegenerator} */
|
||||
json: (blockObject, helpers) => {
|
||||
const payload = blockObject.publishAction
|
||||
|
||||
return {
|
||||
type: "feed_set_value",
|
||||
fields: {
|
||||
FEED_KEY: payload.feed.feed.key,
|
||||
},
|
||||
inputs: {
|
||||
VALUE: helpers.expressionToBlock(payload.value, { shadow: 'io_text' }),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// becomes feed_get_value
|
||||
feed_selector: {
|
||||
/** @type {import('#types').BlockRegenerator} */
|
||||
json: blockObject => {
|
||||
const payload = blockObject.feed
|
||||
|
||||
return {
|
||||
type: "feed_get_value",
|
||||
fields: {
|
||||
FEED_KEY: payload.key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
app/types.js
Normal file
98
app/types.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @import Blockly from "blockly"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BlockConnections
|
||||
* @prop {("value"|"statement")=} mode Is this block a value or statement?
|
||||
* @prop {string=} next Limits the blocks that can connect to the bottom of
|
||||
* this statement block.
|
||||
* @prop {string|string[]=} output what kinds of blocks can be inserted here?
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BlockExtensionFunctionInjectable
|
||||
* @prop {Blockly.Block} block
|
||||
* @prop {function} observeData
|
||||
* @prop {object} data
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback BlockExtensionFunction
|
||||
* @param {BlockExtensionFunctionInjectable} injectables
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string[]|Object.<string, BlockExtensionFunction>} BlockExtensions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string|object} BlocklyMixin
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BlocklyMixin[]} BlockMixins
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback BlockGenerator
|
||||
* @param {Blockly.Block} block
|
||||
* @param {Blockly.Generator} generator
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object.<string, BlockGenerator>} BlockGenerators
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BlockRegeneratorHelpers
|
||||
* @property {function} expressionToBlock
|
||||
* @property {function} arrayToStatements
|
||||
* @property {function} registerVariable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback BlockRegenerator
|
||||
* @param {Object.<string, Object<?,?>>} blockObject
|
||||
* @param {BlockRegeneratorHelpers} helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object.<string, BlockRegenerator>} BlockRegenerators
|
||||
*/
|
||||
|
||||
/**
|
||||
* A plain-old JavaScript object that provides shortcuts for specifying a
|
||||
* Blockly Block. It can export:
|
||||
* - Blockly's block JSON format
|
||||
* - Blockly's block instance JSON format
|
||||
* - documentation fragments
|
||||
*
|
||||
* @typedef {object} BlockDefinitionRaw
|
||||
* @prop {string} type unique string to identify this kind of block internally
|
||||
* @prop {string=} bytecodeKey the unique key this block gets serialized to
|
||||
* @prop {string} name unique string we use when talking about a block
|
||||
* @prop {boolean=} inputsInline Blockly pass-through property that determines
|
||||
* how the block is rendered
|
||||
* @prop {(number|string)=} color A number or string from 0-360 that specifies a
|
||||
* color in Blockly's radial color space
|
||||
* @prop {(number|string)=} colour Alias for "color"
|
||||
* @prop {string=} primaryCategory For blocks appearing in multiple categories,
|
||||
* this property determines which menu this block will appear under in the
|
||||
* docs.
|
||||
* @prop {boolean=} ioPlus Indicates this block requires an IO+ account.
|
||||
* @prop {boolean=} disabled Marks this block for exclusion from all app and
|
||||
* docs builds.
|
||||
* @prop {string} description Markdown documentation for this block. The first
|
||||
* line will be automatically set as the Block's tooltip.
|
||||
* @prop {BlockConnections=} connections
|
||||
* @prop {BlockExtensions=} extensions
|
||||
* @prop {object=} mutator
|
||||
* @prop {BlockMixins=} mixins
|
||||
* @prop {string} template
|
||||
* @prop {object=} fields
|
||||
* @prop {object=} inputs
|
||||
* @prop {BlockGenerators=} generators
|
||||
* @prop {BlockRegenerators=} regenerators
|
||||
* @prop {object=} docOverrides
|
||||
*/
|
||||
|
|
@ -9,10 +9,6 @@ const makeUpTo = (from, target, step) => {
|
|||
return range(from, target, step)
|
||||
}
|
||||
|
||||
const stringifyOptions = (options) => {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} options
|
||||
|
|
@ -22,7 +18,7 @@ const stringifyOptions = (options) => {
|
|||
* @param {number} [options.step]
|
||||
* @param {boolean} [options.reverse]
|
||||
* @param {Function} [options.valueFunc]
|
||||
* @returns {Array}
|
||||
* @returns {[string,string][]}
|
||||
*/
|
||||
export const makeOptions = (options = {}) => {
|
||||
let optionValues
|
||||
|
|
|
|||
5
cypress/cypress.config.js
Normal file
5
cypress/cypress.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {},
|
||||
});
|
||||
21
cypress/e2e/block_images.cy.js
Normal file
21
cypress/e2e/block_images.cy.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
describe("Block Images", () => {
|
||||
it("download all block images", () => {
|
||||
cy.visit("http://localhost:5173/")
|
||||
|
||||
// any extra setup to do before taking screenshots
|
||||
cy.window().then(win => {
|
||||
// disable the weather block's automatic disabling behavior
|
||||
const weatherBlock = win.blocklyWorkspace.getBlockById('block-type-weather')
|
||||
weatherBlock.autoDisable = false
|
||||
// enable it
|
||||
weatherBlock.setEnabled(true)
|
||||
})
|
||||
|
||||
cy.get("[data-id^='block-type-']").each(($el) => {
|
||||
cy.wrap($el).rightclick({ force: true })
|
||||
cy.contains("Save Block as PNG...").click()
|
||||
}).then(blockElements => {
|
||||
cy.log(`Saved ${blockElements.length} block images.`)
|
||||
})
|
||||
})
|
||||
})
|
||||
0
cypress/support/e2e.js
Normal file
0
cypress/support/e2e.js
Normal file
|
|
@ -13,7 +13,7 @@ export default defineConfig({
|
|||
['link', { rel: 'icon', href: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧩</text></svg>" }]
|
||||
],
|
||||
|
||||
base: "/io-actions/",
|
||||
base: "/actions-docs/",
|
||||
|
||||
lastUpdated: true,
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { dispose, inject } from "../blockly/blockly_app.js"
|
||||
import initialWorkspace from "../blockly/workspace.json"
|
||||
import { imageExportRegistryItems } from '#src/image_exporter.js'
|
||||
|
||||
|
||||
const
|
||||
{ block, blocks=[], width="100%", height="200px", toolbox=true } = defineProps(
|
||||
|
|
@ -10,6 +12,30 @@
|
|||
injectOptions = {},
|
||||
options = {
|
||||
injectOptions,
|
||||
contextMenu: {
|
||||
register: [ ...imageExportRegistryItems ]
|
||||
},
|
||||
// TODO: specify dummy extension data with the block defs
|
||||
extensionData: {
|
||||
feedOptions: [
|
||||
["Group A Feed 1", "group-a.feed-1"],
|
||||
["Group A Feed 2", "group-a.feed-2"],
|
||||
["Group B Feed 1", "group-b.feed-1"],
|
||||
["Group C Feed 1", "group-C.feed-1"],
|
||||
],
|
||||
weatherLocationOptions: [
|
||||
[ "Industry City", "1" ],
|
||||
[ "Varick", "2" ],
|
||||
[ "Shenzhen", "3" ],
|
||||
],
|
||||
currentWeatherByLocation: {
|
||||
1: {
|
||||
current: {
|
||||
cloudCover: "5.4321",
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
workspaceJson: block
|
||||
? {
|
||||
blocks: {
|
||||
|
|
|
|||
54
export.js
54
export.js
|
|
@ -1,9 +1,14 @@
|
|||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { copyFileSync, cpSync } from 'node:fs'
|
||||
|
||||
import { cleanDir, write, totalBytesWritten } from "./export_util.js"
|
||||
import DefinitionSet from '#src/definitions/definition_set.js'
|
||||
import { exportTo } from '#src/exporters/index.js'
|
||||
|
||||
|
||||
const toExport = process.argv[2]
|
||||
const
|
||||
toExport = process.argv[2],
|
||||
taskArgs = process.argv.slice(3)
|
||||
|
||||
if(!toExport) {
|
||||
console.error(`Export Error: Missing export name!\nUsage: node export.js [export name]`)
|
||||
|
|
@ -30,8 +35,15 @@ const
|
|||
},
|
||||
|
||||
"docs": async () => {
|
||||
await exporters.app("docs/blockly")
|
||||
// allow option to skip image generation
|
||||
const skipImages = taskArgs.includes("skipImages")
|
||||
if(!skipImages) {
|
||||
await exporters.blockImages()
|
||||
cleanDir("docs/block_images")
|
||||
cpSync("tmp/block_images/images", "docs/block_images", { recursive: true })
|
||||
}
|
||||
|
||||
await exporters.app("docs/blockly")
|
||||
cleanDir("docs/blocks")
|
||||
|
||||
await exportTo("docs", definitions, exportItem => {
|
||||
|
|
@ -41,6 +53,41 @@ const
|
|||
// exportItem.blockExamples(block => "blocks/${block.definitionPath}/examples.json")
|
||||
})
|
||||
},
|
||||
|
||||
"blockImages": async () => {
|
||||
const destination = "tmp/block_images"
|
||||
cleanDir(destination)
|
||||
cleanDir(`${destination}/images`)
|
||||
|
||||
// export a special app with no toolbox, all blocks on workspace
|
||||
await exportTo(destination, definitions, exportItem => {
|
||||
exportItem.workspaceAllBlocks("workspace.json")
|
||||
write(`${destination}/toolbox.json`, "null")
|
||||
exportItem.blocks("blocks.json")
|
||||
exportItem.script("blockly_app.js")
|
||||
// TODO: make a DocumentExporter for generating html wrappers
|
||||
copyFileSync("src/exporters/document_templates/blockly_workspace.template.html", `${destination}/index.html`)
|
||||
})
|
||||
|
||||
// serve it
|
||||
console.log('Serving workspace for screenshots...')
|
||||
const viteProcess = spawn("npx", ["vite", "serve", destination])
|
||||
|
||||
// extract the screenshots
|
||||
console.log('Generating screenshots...')
|
||||
spawnSync("npx", ["cypress", "run",
|
||||
"--config", `downloadsFolder=${destination}/images`,
|
||||
"--config-file", `cypress/cypress.config.js`,
|
||||
])
|
||||
console.log('Generation complete.')
|
||||
|
||||
// kill the server
|
||||
if(!viteProcess.kill()) {
|
||||
console.log("Vite failed to exit gracefully")
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Server closed.')
|
||||
}
|
||||
},
|
||||
exporterNames = Object.keys(exporters)
|
||||
|
||||
|
|
@ -59,3 +106,6 @@ await exporter()
|
|||
const elapsed = Date.now() - startTime
|
||||
console.log("=======================")
|
||||
console.log(`🏁 Done. Wrote ${totalBytesWritten.toFixed(3)}k in ${elapsed}ms 🏁`)
|
||||
|
||||
|
||||
process.exit(0)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const
|
|||
if(fs.existsSync(dirName)) {
|
||||
fs.rmSync(dirName, { recursive: true, force: true })
|
||||
}
|
||||
fs.mkdirSync(dirName)
|
||||
fs.mkdirSync(dirName, { recursive: true, force: true })
|
||||
console.log(`/${dirName}: clean`)
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"target": "es2022",
|
||||
"paths": {
|
||||
"#app/*.js": ["./app/*.js"],
|
||||
"#types": ["./app/types.js"],
|
||||
"#src/*.js": ["./src/*.js"],
|
||||
"#test/*.js": ["./test/*.js"]
|
||||
}
|
||||
|
|
|
|||
1961
package-lock.json
generated
1961
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -35,6 +35,7 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^9.2.0",
|
||||
"chai": "^5.1.2",
|
||||
"cypress": "^14.5.3",
|
||||
"eslint": "^8.57.0",
|
||||
"esm-reload": "^1.0.1",
|
||||
"glob": "^10.4.2",
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@blockly/theme-modern": "^5.0.5",
|
||||
"blockly": "^10.1.3",
|
||||
"blockly": "^10.4.3",
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
495
src/blockly_css.js
Normal file
495
src/blockly_css.js
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
export default `
|
||||
.blocklySvg {
|
||||
background-color: #fff;
|
||||
outline: none;
|
||||
overflow: hidden; /* IE overflows by default. */
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 99999; /* big value for bootstrap3 compatibility */
|
||||
}
|
||||
|
||||
.injectionDiv {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden; /* So blocks in drag surface disappear at edges */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.blocklyNonSelectable {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.blocklyBlockCanvas.blocklyCanvasTransitioning,
|
||||
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
|
||||
transition: transform .5s;
|
||||
}
|
||||
|
||||
.blocklyTooltipDiv {
|
||||
background-color: #ffffc7;
|
||||
border: 1px solid #ddc;
|
||||
box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);
|
||||
color: #000;
|
||||
display: none;
|
||||
font: 9pt sans-serif;
|
||||
opacity: .9;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
z-index: 100000; /* big value for bootstrap3 compatibility */
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
border: 1px solid;
|
||||
border-color: #dadce0;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv.blocklyFocused {
|
||||
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.blocklyDropDownContent {
|
||||
max-height: 300px; /* @todo: spec for maximum height. */
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blocklyDropDownArrow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: -1;
|
||||
background-color: inherit;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.blocklyDropDownButton {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
border: 1px solid;
|
||||
transition: box-shadow .1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blocklyArrowTop {
|
||||
border-top: 1px solid;
|
||||
border-left: 1px solid;
|
||||
border-top-left-radius: 4px;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.blocklyArrowBottom {
|
||||
border-bottom: 1px solid;
|
||||
border-right: 1px solid;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.blocklyResizeSE {
|
||||
cursor: se-resize;
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.blocklyResizeSW {
|
||||
cursor: sw-resize;
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.blocklyResizeLine {
|
||||
stroke: #515A5A;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklyHighlightedConnectionPath {
|
||||
fill: none;
|
||||
stroke: #fc3;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.blocklyPathLight {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklySelected>.blocklyPathLight {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyDraggable {
|
||||
cursor: grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.blocklyDragging {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
/* Changes cursor on mouse down. Not effective in Firefox because of
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
|
||||
.blocklyDraggable:active {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.blocklyDragging.blocklyDraggingDelete {
|
||||
cursor: url("./handdelete.cur"), auto;
|
||||
}
|
||||
|
||||
.blocklyDragging>.blocklyPath,
|
||||
.blocklyDragging>.blocklyPathLight {
|
||||
fill-opacity: .8;
|
||||
stroke-opacity: .8;
|
||||
}
|
||||
|
||||
.blocklyDragging>.blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyDisabled>.blocklyPath {
|
||||
fill-opacity: .5;
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.blocklyDisabled>.blocklyPathLight,
|
||||
.blocklyDisabled>.blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyInsertionMarker>.blocklyPath,
|
||||
.blocklyInsertionMarker>.blocklyPathLight,
|
||||
.blocklyInsertionMarker>.blocklyPathDark {
|
||||
fill-opacity: .2;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.blocklyMultilineText {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.blocklyNonEditableText>text {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyFlyout {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.blocklyText text {
|
||||
cursor: default;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Don't allow users to select text. It gets annoying when trying to
|
||||
drag a block and selected text moves instead.
|
||||
*/
|
||||
.blocklySvg text {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.blocklyHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyFieldDropdown:not(.blocklyHidden) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blocklyIconGroup {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.blocklyIconGroup:not(:hover),
|
||||
.blocklyIconGroupReadonly {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.blocklyIconShape {
|
||||
fill: #00f;
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.blocklyIconSymbol {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.blocklyMinimalBody {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.blocklyHtmlInput {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Remove the increase and decrease arrows on the field number editor */
|
||||
input.blocklyHtmlInput[type=number]::-webkit-inner-spin-button,
|
||||
input.blocklyHtmlInput[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.blocklyMainBackground {
|
||||
stroke-width: 1;
|
||||
stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */
|
||||
}
|
||||
|
||||
.blocklyMutatorBackground {
|
||||
fill: #fff;
|
||||
stroke: #ddd;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: #ddd;
|
||||
fill-opacity: .8;
|
||||
}
|
||||
|
||||
.blocklyMainWorkspaceScrollbar {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.blocklyFlyoutScrollbar {
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHorizontal,
|
||||
.blocklyScrollbarVertical {
|
||||
position: absolute;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.blocklyScrollbarBackground {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle {
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
|
||||
.blocklyScrollbarHandle:hover {
|
||||
fill: #bbb;
|
||||
}
|
||||
|
||||
/* Darken flyout scrollbars due to being on a grey background. */
|
||||
/* By contrast, workspace scrollbars are on a white background. */
|
||||
.blocklyFlyout .blocklyScrollbarHandle {
|
||||
fill: #bbb;
|
||||
}
|
||||
|
||||
.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
|
||||
.blocklyFlyout .blocklyScrollbarHandle:hover {
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.blocklyInvalidInput {
|
||||
background: #faa;
|
||||
}
|
||||
|
||||
.blocklyVerticalMarker {
|
||||
stroke-width: 3px;
|
||||
fill: rgba(255,255,255,.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyComputeCanvas {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.blocklyNoPointerEvents {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyContextMenu {
|
||||
border-radius: 4px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.blocklyDropdownMenu {
|
||||
border-radius: 2px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.blocklyDropdownMenu .blocklyMenuItem {
|
||||
/* 28px on the left for icon or checkbox. */
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
/* BiDi override for the resting state. */
|
||||
.blocklyDropdownMenu .blocklyMenuItemRtl {
|
||||
/* Flip left/right padding for BiDi. */
|
||||
padding-left: 5px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv .blocklyMenu {
|
||||
background: #fff;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
||||
font: normal 13px Arial, sans-serif;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 4px 0;
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 100%;
|
||||
z-index: 20000; /* Arbitrary, but some apps depend on it... */
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
|
||||
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv .blocklyMenu {
|
||||
background: inherit; /* Compatibility with gapi, reset from goog-menu */
|
||||
border: inherit; /* Compatibility with gapi, reset from goog-menu */
|
||||
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
|
||||
outline: none;
|
||||
position: relative; /* Compatibility with gapi, reset from goog-menu */
|
||||
z-index: 20000; /* Arbitrary, but some apps depend on it... */
|
||||
}
|
||||
|
||||
/* State: resting. */
|
||||
.blocklyMenuItem {
|
||||
border: none;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
/* 7em on the right for shortcut. */
|
||||
min-width: 7em;
|
||||
padding: 6px 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* State: disabled. */
|
||||
.blocklyMenuItemDisabled {
|
||||
color: #ccc;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
/* State: hover. */
|
||||
.blocklyMenuItemHighlight {
|
||||
background-color: rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
/* State: selected/checked. */
|
||||
.blocklyMenuItemCheckbox {
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.blocklyMenuItemSelected .blocklyMenuItemCheckbox {
|
||||
background: url(./sprites.png) no-repeat -48px -16px;
|
||||
float: left;
|
||||
margin-left: -24px;
|
||||
position: static; /* Scroll with the menu. */
|
||||
}
|
||||
|
||||
.blocklyMenuItemRtl .blocklyMenuItemCheckbox {
|
||||
float: right;
|
||||
margin-right: -24px;
|
||||
}
|
||||
|
||||
/* Begin .geras-renderer.classic-theme (stripped) */
|
||||
.blocklyText,
|
||||
.blocklyFlyoutLabelText {
|
||||
font: normal 11pt sans-serif;
|
||||
}
|
||||
.blocklyText {
|
||||
fill: #fff;
|
||||
}
|
||||
.blocklyNonEditableText>rect,
|
||||
.blocklyEditableText>rect {
|
||||
fill: #fff;
|
||||
fill-opacity: .6;
|
||||
stroke: none;
|
||||
}
|
||||
.blocklyNonEditableText>text,
|
||||
.blocklyEditableText>text {
|
||||
fill: #000;
|
||||
}
|
||||
.blocklyFlyoutLabelText {
|
||||
fill: #000;
|
||||
}
|
||||
.blocklyText.blocklyBubbleText {
|
||||
fill: #000;
|
||||
}
|
||||
.blocklyEditableText:not(.editing):hover>rect {
|
||||
stroke: #fff;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.blocklyHtmlInput {
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
.blocklySelected>.blocklyPath {
|
||||
stroke: #fc3;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.blocklyHighlightedConnectionPath {
|
||||
stroke: #fc3;
|
||||
}
|
||||
.blocklyReplaceable .blocklyPath {
|
||||
fill-opacity: .5;
|
||||
}
|
||||
.blocklyReplaceable .blocklyPathLight,
|
||||
.blocklyReplaceable .blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
.blocklyInsertionMarker>.blocklyPath {
|
||||
fill-opacity: 0.2;
|
||||
stroke: none;
|
||||
}
|
||||
.blocklyInsertionMarker>.blocklyPathLight,
|
||||
.blocklyInsertionMarker>.blocklyPathDark {
|
||||
fill-opacity: 0.2;
|
||||
stroke: none;
|
||||
}
|
||||
`
|
||||
|
|
@ -18,6 +18,7 @@ class BlockDefinition {
|
|||
name = null
|
||||
|
||||
description = ''
|
||||
docOverrides = {}
|
||||
ioPlus = false
|
||||
|
||||
colour = null
|
||||
|
|
@ -148,7 +149,14 @@ const
|
|||
}
|
||||
|
||||
} else if(shadow) {
|
||||
return shadowToInput(shadow)
|
||||
const shadowJson = shadowToInput(shadow)
|
||||
|
||||
return {
|
||||
// also copy the shadow into a real block
|
||||
// TODO: nested shadow blocks
|
||||
block: shadowJson.shadow,
|
||||
...shadowJson
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -178,11 +186,15 @@ BlockDefinition.parseRawDefinition = function(rawBlockDefinition, definitionPath
|
|||
blockDef.type = rawBlockDefinition.type
|
||||
blockDef.name = rawBlockDefinition.name
|
||||
blockDef.primaryCategory = rawBlockDefinition.primaryCategory
|
||||
blockDef.docOverrides = rawBlockDefinition.docOverrides
|
||||
blockDef.description = rawBlockDefinition.description
|
||||
? niceTemplate(rawBlockDefinition.description)
|
||||
: ""
|
||||
blockDef.ioPlus = rawBlockDefinition.ioPlus
|
||||
blockDef.tooltip = blockDef.description.split("\n")[0]
|
||||
// take the first line of the description
|
||||
// blockDef.tooltip = blockDef.description.split("\n")[0]
|
||||
// take the first sentence of the description
|
||||
blockDef.tooltip = blockDef.description.split(/\.(\s|$)/)[0] + "."
|
||||
blockDef.disabled = !!rawBlockDefinition.disabled
|
||||
blockDef.connections = rawBlockDefinition.connections
|
||||
blockDef.template = rawBlockDefinition.template
|
||||
|
|
|
|||
|
|
@ -60,14 +60,23 @@ DefinitionSet.load = async function(appLocation) {
|
|||
enabledBlocks = reject(rawDefinitions.blocks, "definition.disabled"),
|
||||
definitionSet = new DefinitionSet()
|
||||
|
||||
// TODO: fields
|
||||
// TODO: shadows
|
||||
// TODO: inputs
|
||||
// TODO: process fields
|
||||
// TODO: process shadows
|
||||
// TODO: process inputs
|
||||
|
||||
// process mixins
|
||||
definitionSet.mixins = rawDefinitions.mixins
|
||||
// process extensions
|
||||
definitionSet.extensions = rawDefinitions.extensions
|
||||
// process mutators
|
||||
definitionSet.mutators = rawDefinitions.mutators
|
||||
|
||||
// process standalone regenerators
|
||||
forEach(rawDefinitions.regenerators, (regenerators, blockType) => {
|
||||
definitionSet.regenerators[blockType] = regenerators
|
||||
})
|
||||
|
||||
// process blocks
|
||||
forEach(enabledBlocks, ({ definition, path }) => {
|
||||
const blockDef = BlockDefinition.parseRawDefinition(definition, path, definitionSet)
|
||||
definitionSet.blocks.push(blockDef)
|
||||
|
|
@ -110,15 +119,24 @@ DefinitionSet.load = async function(appLocation) {
|
|||
definitionSet.mutators[blockDef.type] = mutator
|
||||
}
|
||||
|
||||
if(definitionSet.generators[blockDef.type]) {
|
||||
throw new Error(`Generator already present for block: ${blockDef.type}`)
|
||||
}
|
||||
definitionSet.generators[blockDef.type] = blockDef.generators
|
||||
|
||||
if(definitionSet.regenerators[blockDef.type]) {
|
||||
throw new Error(`Regenerator already present for block: ${blockDef.type}`)
|
||||
}
|
||||
definitionSet.regenerators[blockDef.type] = blockDef.regenerators
|
||||
})
|
||||
|
||||
// process toolbox
|
||||
forEach(rawDefinitions.toolboxes, rawToolboxDef => {
|
||||
const toolboxDef = ToolboxDefinition.parseRawDefinition(rawToolboxDef, definitionSet)
|
||||
definitionSet.toolboxes.push(toolboxDef)
|
||||
})
|
||||
|
||||
// process workspace
|
||||
forEach(rawDefinitions.workspaces, rawWorkspaceDef => {
|
||||
const workspaceDef = WorkspaceDefinition.parseRawDefinition(rawWorkspaceDef, definitionSet)
|
||||
definitionSet.workspaces.push(workspaceDef)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { capitalize, trim } from 'lodash-es'
|
||||
import { trim } from 'lodash-es'
|
||||
|
||||
import renderFields from './render_block_fields.js'
|
||||
import renderInputs from './render_block_inputs.js'
|
||||
|
||||
|
||||
const
|
||||
export const
|
||||
IO_PLUS_ALERT = `
|
||||
::: tip :warning: IO+ Required
|
||||
This Block requires an IO+ subscription to use. [Learn more about IO+](https://io.adafruit.com/plus)
|
||||
|
|
@ -17,16 +17,49 @@ This Block requires an IO+ subscription to use. [Learn more about IO+](https://i
|
|||
return trim(`${name} ${ioPlusBadge}`)
|
||||
},
|
||||
|
||||
// 
|
||||
renderBlockImage = ({ name, type }) => ``,
|
||||
|
||||
renderDescription = ({ description }) => description || "No docs for this block, yet.",
|
||||
|
||||
renderIOPlusAlert = ({ ioPlus }) => ioPlus ? IO_PLUS_ALERT : "",
|
||||
|
||||
renderFieldsSection = definition => {
|
||||
const fieldsMarkdown = renderFields(definition)
|
||||
|
||||
return fieldsMarkdown
|
||||
? `## Fields\n\n${ fieldsMarkdown }`
|
||||
: ""
|
||||
},
|
||||
|
||||
renderInputsSection = definition => {
|
||||
const inputsMarkdown = renderInputs(definition)
|
||||
|
||||
return inputsMarkdown
|
||||
? `## Inputs\n\n${ inputsMarkdown }`
|
||||
: ""
|
||||
},
|
||||
|
||||
renderOutput = definition => {
|
||||
return capitalize(definition.connections?.output || "Unspecified")
|
||||
return ''
|
||||
|
||||
// TODO: re-enable when we have something meanginful to show the user
|
||||
// const defaultedOutput = capitalize(definition.connections?.output || "Unspecified")
|
||||
|
||||
// return `
|
||||
// ## Output
|
||||
// ${ defaultedOutput }
|
||||
// `
|
||||
},
|
||||
|
||||
renderExamples = definition => {
|
||||
return "Coming soon..."
|
||||
return ""
|
||||
|
||||
// TODO: re-enable conditionally when we have examples
|
||||
// return `
|
||||
// ## Examples
|
||||
// Coming soon...
|
||||
// `
|
||||
}
|
||||
|
||||
export default definition =>
|
||||
|
|
@ -40,19 +73,17 @@ definitionPath: ${ definition.definitionPath }
|
|||
|
||||
Type: \`${definition.type}\`
|
||||
|
||||
${ renderBlockImage(definition) }
|
||||
|
||||
${ renderDescription(definition) }
|
||||
|
||||
${ renderIOPlusAlert(definition) }
|
||||
|
||||
## Fields
|
||||
${ renderFields(definition) }
|
||||
${ renderFieldsSection(definition) }
|
||||
|
||||
## Inputs
|
||||
${ renderInputs(definition) }
|
||||
${ renderInputsSection(definition) }
|
||||
|
||||
## Output
|
||||
${ renderOutput(definition) }
|
||||
|
||||
## Examples
|
||||
${ renderExamples(definition) }
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,20 +1,40 @@
|
|||
import { capitalize, map, mapValues, values } from 'lodash-es'
|
||||
import { capitalize, keys, map, mapValues, pickBy, values } from 'lodash-es'
|
||||
|
||||
import { niceTemplate } from '#src/util.js'
|
||||
|
||||
|
||||
const
|
||||
renderFields = definition => {
|
||||
if(definition.docOverrides?.fields) {
|
||||
return renderOverridenFields(definition)
|
||||
}
|
||||
|
||||
const fields = values(mapValues(definition.fields, (newField, name) => {
|
||||
newField.field = name
|
||||
return newField
|
||||
}))
|
||||
|
||||
if(!fields.length) { return "This block has no form fields." }
|
||||
if(!fields.length) { return }
|
||||
|
||||
return fields.map(renderField).join("\n\n")
|
||||
},
|
||||
|
||||
renderOverridenFields = definition => {
|
||||
// warn if any inputs have descriptions that won't be rendered
|
||||
const
|
||||
{ fields } = definition.docOverrides,
|
||||
missedFields = keys(pickBy(definition.fields, "description")).join(", ")
|
||||
|
||||
if(missedFields) {
|
||||
console.warn(`Warning [${definition.type}]: Inputs doc is overriden, input descriptions will not be seen for: ${missedFields}`)
|
||||
}
|
||||
|
||||
// determine if the override is a function to call
|
||||
return niceTemplate(typeof fields === 'string'
|
||||
? fields
|
||||
: fields(definition))
|
||||
},
|
||||
|
||||
renderField = field => {
|
||||
const lines = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,42 @@
|
|||
import { capitalize, forEach, keys } from 'lodash-es'
|
||||
import { capitalize, pickBy, forEach, keys } from 'lodash-es'
|
||||
|
||||
import { niceTemplate } from '#src/util.js'
|
||||
|
||||
|
||||
const
|
||||
renderInputs = definition => {
|
||||
if(!keys(definition.inputs).length) {
|
||||
return "This block has no inputs"
|
||||
if(definition.docOverrides?.inputs) {
|
||||
return renderOverridenInputs(definition)
|
||||
}
|
||||
|
||||
if(!keys(definition.inputs).length) { return }
|
||||
|
||||
return renderEachInput(definition)
|
||||
},
|
||||
|
||||
renderOverridenInputs = definition => {
|
||||
// warn if any inputs have descriptions that won't be rendered
|
||||
const
|
||||
{ inputs } = definition.docOverrides,
|
||||
missedInputs = keys(pickBy(definition.inputs, "description")).join(", ")
|
||||
|
||||
if(missedInputs) {
|
||||
console.warn(`Warning [${definition.type}]: Inputs doc is overriden, input descriptions will not be seen for: ${missedInputs}`)
|
||||
}
|
||||
|
||||
// determine if the override is a function to call
|
||||
return niceTemplate(typeof inputs === 'string'
|
||||
? inputs
|
||||
: inputs(definition))
|
||||
},
|
||||
|
||||
renderEachInput = definition => {
|
||||
const lines = []
|
||||
forEach(definition.inputs, (input, inputName) => {
|
||||
if(input.type === 'label') { return }
|
||||
|
||||
lines.push(`### \`${ capitalize(inputName) }\``)
|
||||
lines.push(input.description)
|
||||
if(input.description) { lines.push(niceTemplate(input.description)) }
|
||||
})
|
||||
|
||||
return lines.join("\n\n")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { writeFileSync } from 'fs'
|
||||
import { forEach, isString, without } from 'lodash-es'
|
||||
import { isString, without } from 'lodash-es'
|
||||
|
||||
import { renderBlockImage } from '../docs/render_block.js'
|
||||
|
||||
|
||||
export default class BlockIndexExporter {
|
||||
|
|
@ -78,11 +80,11 @@ const definitionToIndexLines = def => {
|
|||
// block name and link
|
||||
indexLines.push(`### [${ def.name }](/${ def.documentationPath() })`)
|
||||
|
||||
// block image // TODO
|
||||
// indexLines.push(``)
|
||||
|
||||
// block short description
|
||||
indexLines.push(`_${def.tooltip}_`)
|
||||
|
||||
// block image
|
||||
indexLines.push(renderBlockImage(def))
|
||||
|
||||
return indexLines.join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const
|
||||
// TODO: rely on project configuration for docs site location
|
||||
DOCS_BLOCKS_ROOT = "https://adafruit.github.io/io-actions",
|
||||
DOCS_BLOCKS_ROOT = "https://io.adafruit.com/actions-docs",
|
||||
|
||||
processHelp = definition => {
|
||||
if (!definition.definitionPath) { return {} }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Blockly: All Blocks</title>
|
||||
|
||||
<script type="module">
|
||||
import { inject } from './blockly_app.js'
|
||||
import { imageExportRegistryItems } from '#src/image_exporter.js'
|
||||
|
||||
// expose a handle for Cypress to get ahold of the blockly workspace
|
||||
window.blocklyWorkspace = inject("blocklyDiv", {
|
||||
contextMenu: {
|
||||
register: [ ...imageExportRegistryItems ]
|
||||
},
|
||||
extensionData: {
|
||||
feedOptions: [
|
||||
["Group\u00A0A Feed 1", "group-a.feed-1"],
|
||||
["Group\u00A0A Feed 2", "group-a.feed-2"],
|
||||
["Group\u00A0B Feed 1", "group-b.feed-1"],
|
||||
["Group\u00A0C Feed 1", "group-C.feed-1"],
|
||||
],
|
||||
weatherLocationOptions: [
|
||||
[ "Industry City", "1" ],
|
||||
[ "Varick", "2" ],
|
||||
[ "Shenzhen", "3" ],
|
||||
],
|
||||
currentWeatherByLocation: {
|
||||
1: {
|
||||
current: {
|
||||
cloudCover: "5.4321",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#blocklyDiv {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="blocklyDiv"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -2,6 +2,7 @@ import BlockExporter from "./block_exporter.js"
|
|||
import ToolboxExporter from "./toolbox_exporter.js"
|
||||
import WorkspaceExporter from "./workspace_exporter.js"
|
||||
import ScriptExporter from "./script_exporter.js"
|
||||
import WorkspaceAllBlocksExporter from "./workspace_all_blocks_exporter.js"
|
||||
import SidebarExporter from "./sidebar_exporter.js"
|
||||
import BlockIndexExporter from "./block_index_exporter.js"
|
||||
import BlockPageExporter from "./block_page_exporter.js"
|
||||
|
|
@ -24,6 +25,7 @@ export const exportTo = async (destination, definitions, exportFunc) => {
|
|||
blocks: new BlockExporter(definitions, destination).exportToFile,
|
||||
script: new ScriptExporter(definitions, destination).exportToFile,
|
||||
// docs exporters
|
||||
workspaceAllBlocks: new WorkspaceAllBlocksExporter(definitions, destination).exportToFile,
|
||||
sidebar: new SidebarExporter(definitions, destination).exportToFile,
|
||||
blockIndex: new BlockIndexExporter(definitions, destination).exportToFile,
|
||||
blockPages: new BlockPageExporter(definitions, destination).exportToFile,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export const
|
|||
// do normal Blockly injection here
|
||||
currentWorkspace = Blockly.inject(blocklyDivId, blocklyInjectOptions)
|
||||
|
||||
try {
|
||||
// shortcut to make the outside data available everywhere/global
|
||||
// consider if this could be done other ways, less global
|
||||
currentWorkspace.extensionData = options.extensionData
|
||||
|
|
@ -89,6 +90,12 @@ export const
|
|||
})
|
||||
})
|
||||
}
|
||||
} catch(error) {
|
||||
// clean things up
|
||||
dispose()
|
||||
// rethrow exception
|
||||
throw error
|
||||
}
|
||||
|
||||
return currentWorkspace
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ const BYTECODE_BLOCK_TYPE_MAP = {
|
|||
whenDataMatchStateChanged: 'when_data_matching_state',
|
||||
matcherCompare: 'matcher_compare',
|
||||
matcherTextCompare: 'matcher_text_compare',
|
||||
matcherBooleanOperation: 'matcher_boolean_operation',
|
||||
logAction: 'action_log',
|
||||
conditional: 'io_controls_if',
|
||||
compare: 'io_logic_compare',
|
||||
|
|
@ -30,13 +29,15 @@ const BYTECODE_BLOCK_TYPE_MAP = {
|
|||
negate: 'io_logic_negate',
|
||||
setVariable: 'io_variables_set',
|
||||
getVariable: 'io_variables_get',
|
||||
feed: 'feed_selector',
|
||||
getFeedValue: 'feed_get_value',
|
||||
setFeedValue: 'feed_set_value',
|
||||
publishAction: 'action_publish',
|
||||
webhookAction: 'action_webhook',
|
||||
emailAction: 'action_email',
|
||||
smsAction: 'action_sms',
|
||||
// removed blocks that have migration regenerators
|
||||
// see: app/regenerators/migrations.js
|
||||
feed: 'feed_selector',
|
||||
publishAction: 'action_publish',
|
||||
}
|
||||
|
||||
const lookupRegenerator = expressionName => {
|
||||
|
|
|
|||
|
|
@ -25,17 +25,30 @@ export default class SidebarExporter {
|
|||
collapsed: true,
|
||||
items: []
|
||||
}))
|
||||
},
|
||||
uncategorizedCategory = {
|
||||
text: "Uncategorized",
|
||||
collapsed: true,
|
||||
items: []
|
||||
}
|
||||
|
||||
blockSidebar.items.push(uncategorizedCategory)
|
||||
|
||||
forEach(this.definitionSet.blocks, blockDefinition => {
|
||||
const sidebarEntry = {
|
||||
const
|
||||
sidebarEntry = {
|
||||
text: blockDefinition.name,
|
||||
link: blockDefinition.documentationPath()
|
||||
},
|
||||
blockCategories = blockDefinition.getCategories()
|
||||
|
||||
// put into Uncategorized if no category
|
||||
if(!blockCategories.length) {
|
||||
uncategorizedCategory.items.push(sidebarEntry)
|
||||
}
|
||||
|
||||
// add links to each sidebar category we're a part of
|
||||
forEach(blockDefinition.getCategories(), category => {
|
||||
forEach(blockCategories, category => {
|
||||
// if category contains this block, add to its sidebar
|
||||
const sidebarCategory = find(blockSidebar.items, { text: category.name })
|
||||
|
||||
|
|
|
|||
51
src/exporters/workspace_all_blocks_exporter.js
Normal file
51
src/exporters/workspace_all_blocks_exporter.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { writeFileSync } from 'fs'
|
||||
import { isString, invokeMap } from 'lodash-es'
|
||||
|
||||
|
||||
export default class WorkspaceExporter {
|
||||
definitionSet = null
|
||||
destination = null
|
||||
|
||||
constructor(definitionSet, destination) {
|
||||
this.definitionSet = definitionSet
|
||||
this.destination = destination
|
||||
}
|
||||
|
||||
export(givenOptions = {}) {
|
||||
const
|
||||
options = {
|
||||
toFile: false,
|
||||
...givenOptions
|
||||
},
|
||||
allBlocks = invokeMap(this.definitionSet.blocks, "toBlocklyInstanceJSON"),
|
||||
workspaceObject = {
|
||||
blocks: {
|
||||
languageVersion: 0,
|
||||
blocks: allBlocks.map((block, index) => ({
|
||||
...block,
|
||||
id: `block-type-${block.type}`,
|
||||
x: 50,
|
||||
y: 50*index
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if(!options.toFile) {
|
||||
return workspaceObject
|
||||
}
|
||||
|
||||
const filename = isString(options.toFile)
|
||||
? options.toFile
|
||||
: `workspace_all_blocks.json`
|
||||
|
||||
writeFileSync(`${this.destination}/${filename}`, JSON.stringify(workspaceObject, null, 2))
|
||||
}
|
||||
|
||||
exportToFile = (toFile=true) => {
|
||||
this.export({ toFile })
|
||||
}
|
||||
|
||||
static export() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import Blockly from 'blockly'
|
||||
|
||||
import BLOCKLY_CSS from "#src/blockly_css.js"
|
||||
|
||||
|
||||
// right-click menu items
|
||||
export const imageExportRegistryItems = [
|
||||
|
|
@ -8,18 +10,20 @@ export const imageExportRegistryItems = [
|
|||
displayText: 'Save Block as SVG...',
|
||||
weight: 100,
|
||||
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
|
||||
preconditionFn: scope => "enabled",
|
||||
callback: (scope, menuOpenEvent, menuSelectEvent, location) => {
|
||||
downloadBlockAsSVG(scope.block.id)
|
||||
preconditionFn: () => "enabled",
|
||||
callback: scope => {
|
||||
const { id, type } = scope.block
|
||||
downloadBlockAsSVG(id, type)
|
||||
}
|
||||
}, {
|
||||
id: "block-png",
|
||||
displayText: 'Save Block as PNG...',
|
||||
weight: 100,
|
||||
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
|
||||
preconditionFn: scope => "enabled",
|
||||
callback: (scope, menuOpenEvent, menuSelectEvent, location) => {
|
||||
downloadBlockAsPNG(scope.block.id)
|
||||
preconditionFn: () => "enabled",
|
||||
callback: scope => {
|
||||
const { id, type } = scope.block
|
||||
downloadBlockAsPNG(id, type)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -75,13 +79,13 @@ const
|
|||
URL.revokeObjectURL(element.href)
|
||||
},
|
||||
|
||||
downloadBlockAsSVG = blockId => {
|
||||
downloadBlockAsSVG = (blockId, blockType) => {
|
||||
const svgObjectURL = blockToSVGBlob(blockId)
|
||||
|
||||
download(svgObjectURL, 'block.svg')
|
||||
download(svgObjectURL, `${blockType}.svg`)
|
||||
},
|
||||
|
||||
downloadBlockAsPNG = blockId => {
|
||||
downloadBlockAsPNG = (blockId, blockType) => {
|
||||
const
|
||||
svgObjectURL = blockToSVGBlob(blockId),
|
||||
img = new Image()
|
||||
|
|
@ -95,504 +99,8 @@ const
|
|||
|
||||
// extract a png object url from the canvas and download it
|
||||
const pngObjectURL = canvas.toDataURL("image/png")
|
||||
download(pngObjectURL, 'block.png')
|
||||
download(pngObjectURL, `${blockType}.png`)
|
||||
}
|
||||
|
||||
img.src = svgObjectURL
|
||||
}
|
||||
|
||||
// hardcode the CSS we actually use for now (includes geras renderer CSS)
|
||||
const BLOCKLY_CSS = `
|
||||
.blocklySvg {
|
||||
background-color: #fff;
|
||||
outline: none;
|
||||
overflow: hidden; /* IE overflows by default. */
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 99999; /* big value for bootstrap3 compatibility */
|
||||
}
|
||||
|
||||
.injectionDiv {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden; /* So blocks in drag surface disappear at edges */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.blocklyNonSelectable {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.blocklyBlockCanvas.blocklyCanvasTransitioning,
|
||||
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
|
||||
transition: transform .5s;
|
||||
}
|
||||
|
||||
.blocklyTooltipDiv {
|
||||
background-color: #ffffc7;
|
||||
border: 1px solid #ddc;
|
||||
box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);
|
||||
color: #000;
|
||||
display: none;
|
||||
font: 9pt sans-serif;
|
||||
opacity: .9;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
z-index: 100000; /* big value for bootstrap3 compatibility */
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
border: 1px solid;
|
||||
border-color: #dadce0;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv.blocklyFocused {
|
||||
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.blocklyDropDownContent {
|
||||
max-height: 300px; /* @todo: spec for maximum height. */
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blocklyDropDownArrow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
z-index: -1;
|
||||
background-color: inherit;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.blocklyDropDownButton {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
border: 1px solid;
|
||||
transition: box-shadow .1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.blocklyArrowTop {
|
||||
border-top: 1px solid;
|
||||
border-left: 1px solid;
|
||||
border-top-left-radius: 4px;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.blocklyArrowBottom {
|
||||
border-bottom: 1px solid;
|
||||
border-right: 1px solid;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.blocklyResizeSE {
|
||||
cursor: se-resize;
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.blocklyResizeSW {
|
||||
cursor: sw-resize;
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.blocklyResizeLine {
|
||||
stroke: #515A5A;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklyHighlightedConnectionPath {
|
||||
fill: none;
|
||||
stroke: #fc3;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.blocklyPathLight {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklySelected>.blocklyPathLight {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyDraggable {
|
||||
cursor: grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.blocklyDragging {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
/* Changes cursor on mouse down. Not effective in Firefox because of
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */
|
||||
.blocklyDraggable:active {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.blocklyDragging.blocklyDraggingDelete {
|
||||
cursor: url("./handdelete.cur"), auto;
|
||||
}
|
||||
|
||||
.blocklyDragging>.blocklyPath,
|
||||
.blocklyDragging>.blocklyPathLight {
|
||||
fill-opacity: .8;
|
||||
stroke-opacity: .8;
|
||||
}
|
||||
|
||||
.blocklyDragging>.blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyDisabled>.blocklyPath {
|
||||
fill-opacity: .5;
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.blocklyDisabled>.blocklyPathLight,
|
||||
.blocklyDisabled>.blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyInsertionMarker>.blocklyPath,
|
||||
.blocklyInsertionMarker>.blocklyPathLight,
|
||||
.blocklyInsertionMarker>.blocklyPathDark {
|
||||
fill-opacity: .2;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.blocklyMultilineText {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.blocklyNonEditableText>text {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyFlyout {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.blocklyText text {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
Don't allow users to select text. It gets annoying when trying to
|
||||
drag a block and selected text moves instead.
|
||||
*/
|
||||
.blocklySvg text {
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.blocklyHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocklyFieldDropdown:not(.blocklyHidden) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blocklyIconGroup {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.blocklyIconGroup:not(:hover),
|
||||
.blocklyIconGroupReadonly {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.blocklyIconShape {
|
||||
fill: #00f;
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.blocklyIconSymbol {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.blocklyMinimalBody {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.blocklyHtmlInput {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Remove the increase and decrease arrows on the field number editor */
|
||||
input.blocklyHtmlInput[type=number]::-webkit-inner-spin-button,
|
||||
input.blocklyHtmlInput[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.blocklyMainBackground {
|
||||
stroke-width: 1;
|
||||
stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */
|
||||
}
|
||||
|
||||
.blocklyMutatorBackground {
|
||||
fill: #fff;
|
||||
stroke: #ddd;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.blocklyFlyoutBackground {
|
||||
fill: #ddd;
|
||||
fill-opacity: .8;
|
||||
}
|
||||
|
||||
.blocklyMainWorkspaceScrollbar {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.blocklyFlyoutScrollbar {
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHorizontal,
|
||||
.blocklyScrollbarVertical {
|
||||
position: absolute;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.blocklyScrollbarBackground {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.blocklyScrollbarHandle {
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
|
||||
.blocklyScrollbarHandle:hover {
|
||||
fill: #bbb;
|
||||
}
|
||||
|
||||
/* Darken flyout scrollbars due to being on a grey background. */
|
||||
/* By contrast, workspace scrollbars are on a white background. */
|
||||
.blocklyFlyout .blocklyScrollbarHandle {
|
||||
fill: #bbb;
|
||||
}
|
||||
|
||||
.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,
|
||||
.blocklyFlyout .blocklyScrollbarHandle:hover {
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.blocklyInvalidInput {
|
||||
background: #faa;
|
||||
}
|
||||
|
||||
.blocklyVerticalMarker {
|
||||
stroke-width: 3px;
|
||||
fill: rgba(255,255,255,.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyComputeCanvas {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.blocklyNoPointerEvents {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blocklyContextMenu {
|
||||
border-radius: 4px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.blocklyDropdownMenu {
|
||||
border-radius: 2px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.blocklyDropdownMenu .blocklyMenuItem {
|
||||
/* 28px on the left for icon or checkbox. */
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
/* BiDi override for the resting state. */
|
||||
.blocklyDropdownMenu .blocklyMenuItemRtl {
|
||||
/* Flip left/right padding for BiDi. */
|
||||
padding-left: 5px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv .blocklyMenu {
|
||||
background: #fff;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
||||
font: normal 13px Arial, sans-serif;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
padding: 4px 0;
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 100%;
|
||||
z-index: 20000; /* Arbitrary, but some apps depend on it... */
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
|
||||
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.blocklyDropDownDiv .blocklyMenu {
|
||||
background: inherit; /* Compatibility with gapi, reset from goog-menu */
|
||||
border: inherit; /* Compatibility with gapi, reset from goog-menu */
|
||||
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
|
||||
outline: none;
|
||||
position: relative; /* Compatibility with gapi, reset from goog-menu */
|
||||
z-index: 20000; /* Arbitrary, but some apps depend on it... */
|
||||
}
|
||||
|
||||
/* State: resting. */
|
||||
.blocklyMenuItem {
|
||||
border: none;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
/* 7em on the right for shortcut. */
|
||||
min-width: 7em;
|
||||
padding: 6px 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* State: disabled. */
|
||||
.blocklyMenuItemDisabled {
|
||||
color: #ccc;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
/* State: hover. */
|
||||
.blocklyMenuItemHighlight {
|
||||
background-color: rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
/* State: selected/checked. */
|
||||
.blocklyMenuItemCheckbox {
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.blocklyMenuItemSelected .blocklyMenuItemCheckbox {
|
||||
background: url(./sprites.png) no-repeat -48px -16px;
|
||||
float: left;
|
||||
margin-left: -24px;
|
||||
position: static; /* Scroll with the menu. */
|
||||
}
|
||||
|
||||
.blocklyMenuItemRtl .blocklyMenuItemCheckbox {
|
||||
float: right;
|
||||
margin-right: -24px;
|
||||
}
|
||||
|
||||
/* Begin .geras-renderer.classic-theme (stripped) */
|
||||
.blocklyText,
|
||||
.blocklyFlyoutLabelText {
|
||||
font: normal 11pt sans-serif;
|
||||
}
|
||||
.blocklyText {
|
||||
fill: #fff;
|
||||
}
|
||||
.blocklyNonEditableText>rect,
|
||||
.blocklyEditableText>rect {
|
||||
fill: #fff;
|
||||
fill-opacity: .6;
|
||||
stroke: none;
|
||||
}
|
||||
.blocklyNonEditableText>text,
|
||||
.blocklyEditableText>text {
|
||||
fill: #000;
|
||||
}
|
||||
.blocklyFlyoutLabelText {
|
||||
fill: #000;
|
||||
}
|
||||
.blocklyText.blocklyBubbleText {
|
||||
fill: #000;
|
||||
}
|
||||
.blocklyEditableText:not(.editing):hover>rect {
|
||||
stroke: #fff;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.blocklyHtmlInput {
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
.blocklySelected>.blocklyPath {
|
||||
stroke: #fc3;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.blocklyHighlightedConnectionPath {
|
||||
stroke: #fc3;
|
||||
}
|
||||
.blocklyReplaceable .blocklyPath {
|
||||
fill-opacity: .5;
|
||||
}
|
||||
.blocklyReplaceable .blocklyPathLight,
|
||||
.blocklyReplaceable .blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
.blocklyInsertionMarker>.blocklyPath {
|
||||
fill-opacity: 0.2;
|
||||
stroke: none;
|
||||
}
|
||||
.blocklyInsertionMarker>.blocklyPathLight,
|
||||
.blocklyInsertionMarker>.blocklyPathDark {
|
||||
fill-opacity: 0.2;
|
||||
stroke: none;
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const
|
|||
EXTENSION_LOCATION = `extensions/`,
|
||||
MIXIN_LOCATION = `mixins/`,
|
||||
MUTATOR_LOCATION = `mutators/`,
|
||||
REGENERATOR_LOCATION = `regenerators/`,
|
||||
TOOLBOX_LOCATION = `toolbox/`,
|
||||
WORKSPACE_LOCATION = `workspace/`,
|
||||
|
||||
|
|
@ -62,6 +63,23 @@ export const DefinitionLoader = {
|
|||
))
|
||||
},
|
||||
|
||||
loadRegenerators: async (appLocation=APP_LOCATION) => {
|
||||
const jsfiles = await glob(`./${appLocation}/${REGENERATOR_LOCATION}**/*.js`, { ignore: EXAMPLE_FILES })
|
||||
|
||||
// loads app/regenerators/*.js into object like:
|
||||
// { blockType: { json: Function }}
|
||||
let regenerators = {}
|
||||
await Promise.all(jsfiles.map( async filePath => {
|
||||
const regeneratorsFromFile = (await import(`${PROJECT_ROOT}/${filePath}`)).default
|
||||
regenerators = {
|
||||
...regenerators,
|
||||
...regeneratorsFromFile
|
||||
}
|
||||
}))
|
||||
|
||||
return regenerators
|
||||
},
|
||||
|
||||
loadBlocks: async (appLocation=APP_LOCATION) => {
|
||||
// get the file listing
|
||||
const
|
||||
|
|
@ -101,6 +119,7 @@ export const DefinitionLoader = {
|
|||
mutators: await DefinitionLoader.loadMutators(options.source),
|
||||
mixins: await DefinitionLoader.loadMixins(options.source),
|
||||
extensions: await DefinitionLoader.loadExtensions(options.source),
|
||||
regenerators: await DefinitionLoader.loadRegenerators(options.source),
|
||||
blocks: await DefinitionLoader.loadBlocks(options.source),
|
||||
toolboxes: await DefinitionLoader.loadToolboxes(options.source),
|
||||
workspaces: await DefinitionLoader.loadWorkspaces(options.source),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { compact, forOwn, keys, isString, isFunction, isArray, isNumber, isNull, isObject, isUndefined, map, isRegExp, sortBy } from 'lodash-es'
|
||||
import { compact, forOwn, keys, isBoolean, isString, isFunction, isArray, isNumber, isNull, isObject, isUndefined, map, isRegExp, sortBy } from 'lodash-es'
|
||||
|
||||
|
||||
const
|
||||
|
|
@ -13,7 +13,10 @@ const quotedKey = key =>
|
|||
: key
|
||||
|
||||
const renderValue = (value, tab=TAB) => {
|
||||
if (isString(value)) {
|
||||
if (isBoolean(value)) {
|
||||
return renderBoolean(value)
|
||||
|
||||
} else if (isString(value)) {
|
||||
return renderString(value)
|
||||
|
||||
} else if (isRegExp(value) || isNull(value) || isNumber(value) || isUndefined(value) || value === false) {
|
||||
|
|
@ -48,6 +51,10 @@ const renderString = stringValue => {
|
|||
return `"${stringValue}"`
|
||||
}
|
||||
|
||||
const renderBoolean = boolValue => {
|
||||
return `${boolValue}`
|
||||
}
|
||||
|
||||
const renderFunction = (func, indentation=TAB) => {
|
||||
const
|
||||
functionString = func.toString(),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ exports[`Block Snapshots > Blockly JSON > action_email 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "SUBJECT",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -26,10 +27,11 @@ exports[`Block Snapshots > Blockly JSON > action_email 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "BODY",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/email"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/notifications/email"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -46,45 +48,11 @@ exports[`Block Snapshots > Blockly JSON > action_log 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "EXPRESSION",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/log"
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Block Snapshots > Blockly JSON > action_publish 1`] = `
|
||||
{
|
||||
"inputsInline": false,
|
||||
"type": "action_publish",
|
||||
"colour": "0",
|
||||
"tooltip": "Sends the given value to the specified Feed.",
|
||||
"nextStatement": "expression",
|
||||
"previousStatement": "expression",
|
||||
"message0": "📈 Publish %1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_dummy",
|
||||
"align": "CENTRE"
|
||||
}
|
||||
],
|
||||
"message1": "...value: %1",
|
||||
"args1": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"message2": "...to: %1",
|
||||
"args2": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "FEED",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/publish"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/utility/log"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -93,7 +61,7 @@ exports[`Block Snapshots > Blockly JSON > action_root 1`] = `
|
|||
"inputsInline": false,
|
||||
"type": "action_root",
|
||||
"colour": "0",
|
||||
"tooltip": "Add Triggers to determine when this Action runs.",
|
||||
"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.",
|
||||
"message0": "Triggers: %1",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -134,7 +102,7 @@ exports[`Block Snapshots > Blockly JSON > action_root 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "action_root",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/root"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/root"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -158,10 +126,11 @@ exports[`Block Snapshots > Blockly JSON > action_sms 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "BODY",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/sms"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/notifications/sms"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -185,6 +154,7 @@ exports[`Block Snapshots > Blockly JSON > action_webhook 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "URL",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -205,10 +175,11 @@ exports[`Block Snapshots > Blockly JSON > action_webhook 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "BODY",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/action/webhook"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/notifications/webhook"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -227,7 +198,7 @@ exports[`Block Snapshots > Blockly JSON > all_days 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "all_days",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/all_days"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_days"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -246,7 +217,7 @@ exports[`Block Snapshots > Blockly JSON > all_hours 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "all_hours",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/all_hours"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_hours"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -265,7 +236,7 @@ exports[`Block Snapshots > Blockly JSON > all_minutes 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "all_minutes",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/all_minutes"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_minutes"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -284,7 +255,7 @@ exports[`Block Snapshots > Blockly JSON > all_months 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "all_months",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/all_months"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/all_months"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -303,7 +274,7 @@ exports[`Block Snapshots > Blockly JSON > day_settings 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/day_settings"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/day_settings"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -364,7 +335,7 @@ exports[`Block Snapshots > Blockly JSON > days_of_week 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "days_of_week",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/days_of_week"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/days_of_week"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -382,7 +353,7 @@ exports[`Block Snapshots > Blockly JSON > delay_days 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_days"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_days"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -498,7 +469,7 @@ exports[`Block Snapshots > Blockly JSON > delay_hours 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_hours"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_hours"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -758,7 +729,7 @@ exports[`Block Snapshots > Blockly JSON > delay_minutes 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_minutes"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_minutes"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -776,7 +747,7 @@ exports[`Block Snapshots > Blockly JSON > delay_none 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_none"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_none"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -1036,7 +1007,7 @@ exports[`Block Snapshots > Blockly JSON > delay_seconds 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_seconds"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_seconds"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -1090,7 +1061,7 @@ exports[`Block Snapshots > Blockly JSON > delay_settings 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/root/action_settings/delay_settings"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/delay_settings"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -1403,7 +1374,7 @@ exports[`Block Snapshots > Blockly JSON > every_days_between 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "every_days_between",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/every_days_between"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_days_between"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -1660,7 +1631,7 @@ exports[`Block Snapshots > Blockly JSON > every_hours_between 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "every_hours_between",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/every_hours_between"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_hours_between"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2221,7 +2192,7 @@ exports[`Block Snapshots > Blockly JSON > every_minutes_between 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "every_minutes_between",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/every_minutes_between"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_minutes_between"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2374,7 +2345,7 @@ exports[`Block Snapshots > Blockly JSON > every_months_between 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "every_months_between",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/every_months_between"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/every_months_between"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2384,7 +2355,7 @@ exports[`Block Snapshots > Blockly JSON > feed_get_value 1`] = `
|
|||
"type": "feed_get_value",
|
||||
"colour": 300,
|
||||
"tooltip": "Resolves to the last value of this feed or component, always a String",
|
||||
"output": null,
|
||||
"output": "expression",
|
||||
"message0": "Get %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -2406,39 +2377,7 @@ exports[`Block Snapshots > Blockly JSON > feed_get_value 1`] = `
|
|||
"replaceDropdownOptions",
|
||||
"populateFeedDropdown"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/feed/get_value"
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Block Snapshots > Blockly JSON > feed_selector 1`] = `
|
||||
{
|
||||
"inputsInline": false,
|
||||
"type": "feed_selector",
|
||||
"colour": 300,
|
||||
"tooltip": "The last value of this feed or component, always a String",
|
||||
"output": null,
|
||||
"message0": "Feed: %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
"name": "FEED_KEY",
|
||||
"type": "field_dropdown",
|
||||
"options": [
|
||||
[
|
||||
"Loading Feeds...",
|
||||
""
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "input_dummy",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"extensions": [
|
||||
"replaceDropdownOptions",
|
||||
"populateFeedDropdown"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/feed/selector"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/feeds/get_value"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2465,6 +2404,7 @@ exports[`Block Snapshots > Blockly JSON > feed_set_value 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2472,7 +2412,7 @@ exports[`Block Snapshots > Blockly JSON > feed_set_value 1`] = `
|
|||
"replaceDropdownOptions",
|
||||
"populateFeedDropdown"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/feed/set_value"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/feeds/set_value"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2491,7 +2431,7 @@ exports[`Block Snapshots > Blockly JSON > hour_settings 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/hour_settings"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/hour_settings"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2508,6 +2448,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "IF0",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2516,6 +2457,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = `
|
|||
{
|
||||
"type": "input_statement",
|
||||
"name": "THEN0",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2536,7 +2478,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "io_controls_if",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/controls/if"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/if"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2546,7 +2488,10 @@ exports[`Block Snapshots > Blockly JSON > io_logic_boolean 1`] = `
|
|||
"type": "io_logic_boolean",
|
||||
"colour": 60,
|
||||
"tooltip": "A true or false value.",
|
||||
"output": "boolean",
|
||||
"output": [
|
||||
"expression",
|
||||
"boolean"
|
||||
],
|
||||
"message0": "%1 %2",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -2568,7 +2513,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_boolean 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/logic/boolean"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/boolean"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2577,13 +2522,14 @@ exports[`Block Snapshots > Blockly JSON > io_logic_compare 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "io_logic_compare",
|
||||
"colour": 120,
|
||||
"tooltip": "Numerically compare two given values using the selected math operation.",
|
||||
"output": null,
|
||||
"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.",
|
||||
"output": "expression",
|
||||
"message0": "%1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "A",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2622,10 +2568,11 @@ exports[`Block Snapshots > Blockly JSON > io_logic_compare 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/compare"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/compare"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2634,17 +2581,18 @@ exports[`Block Snapshots > Blockly JSON > io_logic_negate 1`] = `
|
|||
"inputsInline": false,
|
||||
"type": "io_logic_negate",
|
||||
"colour": 60,
|
||||
"tooltip": "Swaps a truthy value to \`false\`, or a falsy value to \`true\`.",
|
||||
"output": null,
|
||||
"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.",
|
||||
"output": "expression",
|
||||
"message0": "not %1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "EXPRESSION",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/logic/negate"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/negate"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2653,13 +2601,14 @@ exports[`Block Snapshots > Blockly JSON > io_logic_operation 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "io_logic_operation",
|
||||
"colour": 60,
|
||||
"tooltip": "Perform the specifed boolean logic operation on two operands.",
|
||||
"output": null,
|
||||
"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.",
|
||||
"output": "expression",
|
||||
"message0": "%1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "A",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2682,10 +2631,11 @@ exports[`Block Snapshots > Blockly JSON > io_logic_operation 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/logic/operation"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/operation"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2695,12 +2645,13 @@ exports[`Block Snapshots > Blockly JSON > io_math_arithmetic 1`] = `
|
|||
"type": "io_math_arithmetic",
|
||||
"colour": 120,
|
||||
"tooltip": "Perform the specified arithmetic operation on two specified operands.",
|
||||
"output": null,
|
||||
"output": "expression",
|
||||
"message0": "%1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "A",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2735,10 +2686,11 @@ exports[`Block Snapshots > Blockly JSON > io_math_arithmetic 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/arithmetic"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/arithmetic"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2754,6 +2706,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_constrain 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2766,7 +2719,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_constrain 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/constrain"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/constrain"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2776,7 +2729,10 @@ exports[`Block Snapshots > Blockly JSON > io_math_number 1`] = `
|
|||
"type": "io_math_number",
|
||||
"colour": 120,
|
||||
"tooltip": "A numeric value, whole or decimal.",
|
||||
"output": "number",
|
||||
"output": [
|
||||
"expression",
|
||||
"number"
|
||||
],
|
||||
"message0": " %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -2792,7 +2748,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_number 1`] = `
|
|||
"extensions": [
|
||||
"validateNumbers"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/number"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/number"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2826,10 +2782,11 @@ exports[`Block Snapshots > Blockly JSON > io_math_round 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/round"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/round"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2839,7 +2796,10 @@ exports[`Block Snapshots > Blockly JSON > io_text 1`] = `
|
|||
"type": "io_text",
|
||||
"colour": 180,
|
||||
"tooltip": "A String of text",
|
||||
"output": "String",
|
||||
"output": [
|
||||
"expression",
|
||||
"string"
|
||||
],
|
||||
"message0": "\\"%1 %2",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -2852,7 +2812,7 @@ exports[`Block Snapshots > Blockly JSON > io_text 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/text"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/text"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2862,12 +2822,13 @@ exports[`Block Snapshots > Blockly JSON > io_text_join 1`] = `
|
|||
"type": "io_text_join",
|
||||
"colour": 180,
|
||||
"tooltip": "Join two pieces of text into one.",
|
||||
"output": null,
|
||||
"output": "expression",
|
||||
"message0": "%1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "A",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -2876,10 +2837,11 @@ exports[`Block Snapshots > Blockly JSON > io_text_join 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/join"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/join"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2889,8 +2851,11 @@ exports[`Block Snapshots > Blockly JSON > io_text_multiline 1`] = `
|
|||
"type": "io_text_multiline",
|
||||
"colour": 180,
|
||||
"tooltip": "A String of longer-form text with newlines.",
|
||||
"output": "String",
|
||||
"message0": "P %1 %2",
|
||||
"output": [
|
||||
"expression",
|
||||
"string"
|
||||
],
|
||||
"message0": "¶ %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
"name": "TEXT",
|
||||
|
|
@ -2902,7 +2867,7 @@ exports[`Block Snapshots > Blockly JSON > io_text_multiline 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/text_multiline"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/text_multiline"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2911,8 +2876,8 @@ exports[`Block Snapshots > Blockly JSON > io_variables_get 1`] = `
|
|||
"inputsInline": false,
|
||||
"type": "io_variables_get",
|
||||
"colour": 240,
|
||||
"tooltip": "Get the value previously assigned to a variable.",
|
||||
"output": "String",
|
||||
"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.",
|
||||
"output": "expression",
|
||||
"message0": "Get variable %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -2924,7 +2889,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_get 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/variables/get"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/variables/get"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2933,7 +2898,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_set 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "io_variables_set",
|
||||
"colour": 240,
|
||||
"tooltip": "Set a variable to a value",
|
||||
"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.",
|
||||
"nextStatement": "expression",
|
||||
"previousStatement": "expression",
|
||||
"message0": "Set variable %1 = %2",
|
||||
|
|
@ -2945,43 +2910,11 @@ exports[`Block Snapshots > Blockly JSON > io_variables_set 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/variables/set"
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Block Snapshots > Blockly JSON > matcher_boolean_operation 1`] = `
|
||||
{
|
||||
"inputsInline": true,
|
||||
"type": "matcher_boolean_operation",
|
||||
"colour": 60,
|
||||
"tooltip": "Perform a logic operation between the triggering Feed value and a block diagram.",
|
||||
"output": "matcher",
|
||||
"message0": "is true %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
"name": "OP",
|
||||
"type": "field_dropdown",
|
||||
"options": [
|
||||
[
|
||||
"and",
|
||||
"AND"
|
||||
],
|
||||
[
|
||||
"or",
|
||||
"OR"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/matchers/matcher_boolean_operation"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/variables/set"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -2990,7 +2923,7 @@ exports[`Block Snapshots > Blockly JSON > matcher_compare 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "matcher_compare",
|
||||
"colour": 224,
|
||||
"tooltip": "Numerically compare the new Feed value with another number.",
|
||||
"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.",
|
||||
"output": "matcher",
|
||||
"message0": "%1 %2",
|
||||
"args0": [
|
||||
|
|
@ -3027,10 +2960,11 @@ exports[`Block Snapshots > Blockly JSON > matcher_compare 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/matchers/matcher_compare"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/matcher_compare"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3039,7 +2973,7 @@ exports[`Block Snapshots > Blockly JSON > matcher_text_compare 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "matcher_text_compare",
|
||||
"colour": 180,
|
||||
"tooltip": "Compare the new feed value with text for equality, inequality, or inclusion.",
|
||||
"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.",
|
||||
"output": "matcher",
|
||||
"message0": "%1 %2",
|
||||
"args0": [
|
||||
|
|
@ -3064,10 +2998,11 @@ exports[`Block Snapshots > Blockly JSON > matcher_text_compare 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/matchers/matcher_text_compare"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/matcher_text_compare"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3090,6 +3025,7 @@ exports[`Block Snapshots > Blockly JSON > math_map 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "VALUE",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -3111,7 +3047,7 @@ exports[`Block Snapshots > Blockly JSON > math_map 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/map"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/math/map"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3127,6 +3063,7 @@ exports[`Block Snapshots > Blockly JSON > math_range 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "FROM",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -3135,6 +3072,7 @@ exports[`Block Snapshots > Blockly JSON > math_range 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "TO",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -3145,7 +3083,7 @@ exports[`Block Snapshots > Blockly JSON > math_range 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/math/range"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/range"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3164,7 +3102,7 @@ exports[`Block Snapshots > Blockly JSON > minute_settings 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/minute_settings"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/minute_settings"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3183,7 +3121,7 @@ exports[`Block Snapshots > Blockly JSON > month_settings 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/month_settings"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/month_settings"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3192,7 +3130,7 @@ exports[`Block Snapshots > Blockly JSON > on_schedule 1`] = `
|
|||
"inputsInline": false,
|
||||
"type": "on_schedule",
|
||||
"colour": 30,
|
||||
"tooltip": "A schedule to run the action, from every minute to once a year.",
|
||||
"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.",
|
||||
"nextStatement": "trigger",
|
||||
"previousStatement": "trigger",
|
||||
"message0": "Schedule %1",
|
||||
|
|
@ -3238,7 +3176,7 @@ exports[`Block Snapshots > Blockly JSON > on_schedule 1`] = `
|
|||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/on_schedule"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/on_schedule"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3387,7 +3325,7 @@ exports[`Block Snapshots > Blockly JSON > one_day 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "one_day",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/day/one_day"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_day"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3508,7 +3446,7 @@ exports[`Block Snapshots > Blockly JSON > one_hour 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "one_hour",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/hour/one_hour"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_hour"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3773,7 +3711,7 @@ exports[`Block Snapshots > Blockly JSON > one_minute 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "one_minute",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/minute/one_minute"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_minute"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3846,7 +3784,7 @@ exports[`Block Snapshots > Blockly JSON > one_month 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "one_month",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/one_month"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/one_month"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3939,7 +3877,7 @@ exports[`Block Snapshots > Blockly JSON > some_months 1`] = `
|
|||
}
|
||||
],
|
||||
"mutator": "some_months",
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/schedule/month/some_months"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/uncategorized/some_months"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3948,13 +3886,14 @@ exports[`Block Snapshots > Blockly JSON > text_compare 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "text_compare",
|
||||
"colour": 180,
|
||||
"tooltip": "Compare two chunks of text for equality, inequality, or inclusion.",
|
||||
"output": null,
|
||||
"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.",
|
||||
"output": "expression",
|
||||
"message0": "%1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "A",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
|
|
@ -3981,10 +3920,11 @@ exports[`Block Snapshots > Blockly JSON > text_compare 1`] = `
|
|||
{
|
||||
"type": "input_value",
|
||||
"name": "B",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/compare"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/logic/compare"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -3994,16 +3934,20 @@ exports[`Block Snapshots > Blockly JSON > text_template 1`] = `
|
|||
"type": "text_template",
|
||||
"colour": 180,
|
||||
"tooltip": "Render a text template.",
|
||||
"output": null,
|
||||
"output": [
|
||||
"expression",
|
||||
"string"
|
||||
],
|
||||
"message0": "{{ %1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_value",
|
||||
"name": "TEMPLATE",
|
||||
"check": "expression",
|
||||
"align": "RIGHT"
|
||||
}
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/text/template"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/text/template"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -4013,7 +3957,7 @@ exports[`Block Snapshots > Blockly JSON > weather 1`] = `
|
|||
"type": "weather",
|
||||
"colour": 360,
|
||||
"tooltip": "Fetch the current or forecast weather conditions at the specified location.",
|
||||
"output": null,
|
||||
"output": "expression",
|
||||
"message0": "Weather %1",
|
||||
"args0": [
|
||||
{
|
||||
|
|
@ -4156,7 +4100,7 @@ exports[`Block Snapshots > Blockly JSON > weather 1`] = `
|
|||
"weatherMixin",
|
||||
"prepareWeather"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/power_ups/weather"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/weather/weather"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -4165,7 +4109,7 @@ exports[`Block Snapshots > Blockly JSON > when_data 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "when_data",
|
||||
"colour": 30,
|
||||
"tooltip": "Run this action when a Feed receives a new data point.",
|
||||
"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.",
|
||||
"nextStatement": "trigger",
|
||||
"previousStatement": "trigger",
|
||||
"message0": "When %1 gets any data %2",
|
||||
|
|
@ -4189,7 +4133,7 @@ exports[`Block Snapshots > Blockly JSON > when_data 1`] = `
|
|||
"replaceDropdownOptions",
|
||||
"populateFeedDropdown"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/when_data"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/when_data"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -4198,7 +4142,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "when_data_matching",
|
||||
"colour": 30,
|
||||
"tooltip": "Run this Action when the specified Feed receives data that matches the specified condition.",
|
||||
"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.",
|
||||
"nextStatement": "trigger",
|
||||
"previousStatement": "trigger",
|
||||
"message0": "When %1 gets data matching: %2",
|
||||
|
|
@ -4224,7 +4168,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching 1`] = `
|
|||
"replaceDropdownOptions",
|
||||
"populateFeedDropdown"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/when_data_matching"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/when_data_matching"
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -4233,7 +4177,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching_state 1`] = `
|
|||
"inputsInline": true,
|
||||
"type": "when_data_matching_state",
|
||||
"colour": 30,
|
||||
"tooltip": "Run this Action when the specified Feed receives a data point that compares to its previous data point in the specified way.",
|
||||
"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'.",
|
||||
"nextStatement": "trigger",
|
||||
"previousStatement": "trigger",
|
||||
"message0": "When %1 gets data that %2 matching %3",
|
||||
|
|
@ -4281,6 +4225,6 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching_state 1`] = `
|
|||
"replaceDropdownOptions",
|
||||
"populateFeedDropdown"
|
||||
],
|
||||
"helpUrl": "https://adafruit.github.io/io-actions/blocks/triggers/when_data_matching_state"
|
||||
"helpUrl": "https://io.adafruit.com/actions-docs/blocks/triggers/when_data_matching_state"
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ describe("Exporting Blockly Files", () => {
|
|||
assert.exists(trunkObject)
|
||||
assert.hasAllKeys(trunkObject, ["type", "inputs"])
|
||||
assert.hasAllKeys(trunkObject.inputs, ["SUBJECT", "PREDICATE"])
|
||||
assert.hasAllKeys(trunkObject.inputs.SUBJECT, ["shadow"])
|
||||
assert.hasAllKeys(trunkObject.inputs.PREDICATE, ["shadow"])
|
||||
assert.hasAllKeys(trunkObject.inputs.SUBJECT, ["shadow", "block"])
|
||||
assert.hasAllKeys(trunkObject.inputs.PREDICATE, ["shadow", "block"])
|
||||
})
|
||||
|
||||
it("export other block trees by name", { skip: true }, () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue