From f3cb4fcd60f23ba0a7936145a5155c477e17775f Mon Sep 17 00:00:00 2001 From: Justin Cooper Date: Fri, 22 Aug 2025 15:03:14 -0500 Subject: [PATCH] poc of how time blocks could work --- app/blocks/utility/current_time.js | 30 ++ app/blocks/utility/time.js | 67 +++ app/toolbox/index.js | 2 + app/toolbox/time.js | 8 + .../block_snapshots_test.js.snapshot | 470 ++++++++++++++++-- .../blocks/utility_current_time_block_test.js | 84 ++++ test/app/blocks/utility_time_block_test.js | 150 ++++++ .../blocks/utility_time_integration_test.js | 168 +++++++ 8 files changed, 942 insertions(+), 37 deletions(-) create mode 100644 app/blocks/utility/current_time.js create mode 100644 app/blocks/utility/time.js create mode 100644 app/toolbox/time.js create mode 100644 test/app/blocks/utility_current_time_block_test.js create mode 100644 test/app/blocks/utility_time_block_test.js create mode 100644 test/app/blocks/utility_time_integration_test.js diff --git a/app/blocks/utility/current_time.js b/app/blocks/utility/current_time.js new file mode 100644 index 0000000..5494047 --- /dev/null +++ b/app/blocks/utility/current_time.js @@ -0,0 +1,30 @@ +/** @type {import('#types').BlockDefinitionRaw} */ +export default { + type: 'io_utility_current_time', + name: "Current Time", + color: 360, + description: "Get the current system time in 24-hour format for use in time comparisons and conditions. Returns the current hour and minute as a time value that can be compared with Time blocks. Perfect for creating time-based automation logic like 'if current time > 14:30' or 'if current time is between 9:00 and 17:00'. Found in the Time category alongside Time block.", + connections: { + mode: "value", + output: ["expression", "time"], + }, + template: "Current Time", + generators: { + json: () => { + return [JSON.stringify({ + currentTime: {} + }), 0] + } + }, + regenerators: { + json: (blockObject, helpers) => { + if (!blockObject.currentTime) { + throw new Error("No currentTime data for io_utility_current_time regenerator") + } + + return { + type: 'io_utility_current_time' + } + } + } +} \ No newline at end of file diff --git a/app/blocks/utility/time.js b/app/blocks/utility/time.js new file mode 100644 index 0000000..e6caa00 --- /dev/null +++ b/app/blocks/utility/time.js @@ -0,0 +1,67 @@ +import { makeOptions } from "#app/util/fields.js" + +/** @type {import('#types').BlockDefinitionRaw} */ +export default { + type: 'io_utility_time', + name: "Time", + color: 360, + description: "Create a time value in 24-hour format for use in comparisons and conditions. Perfect for building time-based logic like 'if current time > 14:30' or 'if time equals 09:00'. Hours range from 00-23, minutes from 00-59. The time value can be compared with other times or used in mathematical operations. Found in the Time category alongside Current Time block.", + connections: { + mode: "value", + output: ["expression", "time"], + }, + template: "%HOUR : %MINUTE", + fields: { + HOUR: { + description: "Select the hour in 24-hour format (00-23). Examples: 00 for midnight, 12 for noon, 14 for 2 PM, 23 for 11 PM. This military time format ensures precise time comparisons without AM/PM confusion.", + options: makeOptions({ + upTo: 24, + valueFunc: hour => hour.toString().padStart(2, '0') + }) + }, + MINUTE: { + description: "Select the minute (00-59). Examples: 00 for on the hour, 15 for quarter past, 30 for half past, 45 for quarter to. Combined with hours, creates precise time values for your automation logic.", + options: makeOptions({ + upTo: 60, + valueFunc: minute => minute.toString().padStart(2, '0') + }) + } + }, + generators: { + json: block => { + const + hour = block.getFieldValue('HOUR'), + minute = block.getFieldValue('MINUTE'), + timeValue = `${hour}:${minute}`, + // Convert to minutes since midnight for numeric comparison + totalMinutes = parseInt(hour, 10) * 60 + parseInt(minute, 10) + + return [JSON.stringify({ + time: { + display: timeValue, + value: totalMinutes + } + }), 0] + } + }, + regenerators: { + json: (blockObject, helpers) => { + const timeData = blockObject.time + if (!timeData) { + throw new Error("No time data for io_utility_time regenerator") + } + + const totalMinutes = timeData.value + const hour = Math.floor(totalMinutes / 60).toString().padStart(2, '0') + const minute = (totalMinutes % 60).toString().padStart(2, '0') + + return { + type: 'io_utility_time', + fields: { + HOUR: hour, + MINUTE: minute + } + } + } + } +} \ No newline at end of file diff --git a/app/toolbox/index.js b/app/toolbox/index.js index 67c73fa..2e9c882 100644 --- a/app/toolbox/index.js +++ b/app/toolbox/index.js @@ -4,6 +4,7 @@ import Math from './math.js' import Notifications from './notifications.js' import Weather from './weather.js' import Text from './text.js' +import Time from './time.js' import Triggers from './triggers.js' import Utility from './utility.js' import Variables from './variables.js' @@ -15,6 +16,7 @@ export default [ Logic, Math, Text, + Time, Variables, Feeds, Notifications, diff --git a/app/toolbox/time.js b/app/toolbox/time.js new file mode 100644 index 0000000..ee089d3 --- /dev/null +++ b/app/toolbox/time.js @@ -0,0 +1,8 @@ +export default { + name: 'Time', + colour: 360, + contents: [ + 'io_utility_time', + 'io_utility_current_time' + ] +} \ No newline at end of file diff --git a/test/app/blocks/snapshots/block_snapshots_test.js.snapshot b/test/app/blocks/snapshots/block_snapshots_test.js.snapshot index 21e5878..1f6357a 100644 --- a/test/app/blocks/snapshots/block_snapshots_test.js.snapshot +++ b/test/app/blocks/snapshots/block_snapshots_test.js.snapshot @@ -3,7 +3,7 @@ exports[`Block Snapshots > Blockly JSON > action_email 1`] = ` "inputsInline": false, "type": "action_email", "colour": "0", - "tooltip": "Sends an email with given subject and body templates", + "tooltip": "Sends an email with given subject and body templates.", "nextStatement": "expression", "previousStatement": "expression", "message0": "📧 Email %1", @@ -61,7 +61,7 @@ exports[`Block Snapshots > Blockly JSON > action_root 1`] = ` "inputsInline": false, "type": "action_root", "colour": "0", - "tooltip": "The foundation of every Adafruit IO Action. Connect Triggers (like 'when temperature > 80°F' or 'every morning at 8 AM') to define when your Action runs, then attach Action blocks (like 'send email', 'publish to feed', or 'if/then logic') to define what happens when triggered.", + "tooltip": "The foundation of every Adafruit IO Action.", "message0": "Triggers: %1", "args0": [ { @@ -94,7 +94,7 @@ exports[`Block Snapshots > Blockly JSON > action_root 1`] = ` "align": "RIGHT" } ], - "message4": "ㅤ %1", + "message4": "  %1", "args4": [ { "type": "input_dummy", @@ -264,7 +264,7 @@ exports[`Block Snapshots > Blockly JSON > day_settings 1`] = ` "inputsInline": false, "type": "day_settings", "colour": 30, - "tooltip": "How would you like to specify days of the month for your schedule?", + "tooltip": "How would you like to specify days of the month for your schedule?.", "message0": "Day: %1", "args0": [ { @@ -344,7 +344,7 @@ exports[`Block Snapshots > Blockly JSON > delay_days 1`] = ` "inputsInline": false, "type": "delay_days", "colour": "0", - "tooltip": "1 day is the maximum delay available", + "tooltip": "1 day is the maximum delay available.", "output": "delay_period", "message0": "1 day %1", "args0": [ @@ -362,7 +362,7 @@ exports[`Block Snapshots > Blockly JSON > delay_hours 1`] = ` "inputsInline": false, "type": "delay_hours", "colour": "0", - "tooltip": "Set a delay between 1 and 23 hours", + "tooltip": "Set a delay between 1 and 23 hours.", "output": "delay_period", "message0": "%1 hours %2", "args0": [ @@ -478,7 +478,7 @@ exports[`Block Snapshots > Blockly JSON > delay_minutes 1`] = ` "inputsInline": false, "type": "delay_minutes", "colour": "0", - "tooltip": "Set a delay between 1 and 59 minutes", + "tooltip": "Set a delay between 1 and 59 minutes.", "output": "delay_period", "message0": "%1 minutes %2", "args0": [ @@ -756,7 +756,7 @@ exports[`Block Snapshots > Blockly JSON > delay_seconds 1`] = ` "inputsInline": false, "type": "delay_seconds", "colour": "0", - "tooltip": "Set a delay between 1 and 59 seconds (or 0 for no delay)", + "tooltip": "Set a delay between 1 and 59 seconds (or 0 for no delay).", "output": "delay_period", "message0": "%1 seconds %2", "args0": [ @@ -1016,7 +1016,7 @@ exports[`Block Snapshots > Blockly JSON > delay_settings 1`] = ` "inputsInline": false, "type": "delay_settings", "colour": "0", - "tooltip": "Causes a delay between this Action's trigger and its execution", + "tooltip": "Causes a delay between this Action's trigger and its execution.", "message0": "Delay Settings %1", "args0": [ { @@ -2354,7 +2354,7 @@ exports[`Block Snapshots > Blockly JSON > feed_get_value 1`] = ` "inputsInline": false, "type": "feed_get_value", "colour": 300, - "tooltip": "Resolves to the last value of this feed or component, always a String", + "tooltip": "Resolves to the last value of this feed or component, always a String.", "output": "expression", "message0": "Get %1 %2", "args0": [ @@ -2421,7 +2421,7 @@ exports[`Block Snapshots > Blockly JSON > hour_settings 1`] = ` "inputsInline": false, "type": "hour_settings", "colour": 30, - "tooltip": "How would you like to specify hours of the day for your schedule?", + "tooltip": "How would you like to specify hours of the day for your schedule?.", "message0": "Hour: %1", "args0": [ { @@ -2440,7 +2440,7 @@ exports[`Block Snapshots > Blockly JSON > io_controls_if 1`] = ` "inputsInline": false, "type": "io_controls_if", "colour": 60, - "tooltip": "Execute different block diagrams based on the outcome of conditional checks.", + "tooltip": "Create smart decision-making logic for your IoT Actions using if/then/else statements.", "nextStatement": "expression", "previousStatement": "expression", "message0": "if %1", @@ -2487,7 +2487,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_boolean 1`] = ` "inputsInline": false, "type": "io_logic_boolean", "colour": 60, - "tooltip": "A true or false value.", + "tooltip": "A simple true or false value for building logic conditions and controlling digital outputs.", "output": [ "expression", "boolean" @@ -2522,7 +2522,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_compare 1`] = ` "inputsInline": true, "type": "io_logic_compare", "colour": 120, - "tooltip": "Build mathematical conditions by comparing any two numerical values in your Action logic. Perfect for creating if/then statements like 'if temperature is greater than target temp', 'if battery level equals low threshold', or 'if sensor reading is between two values'. Works with feed data, variables, calculations, or any numerical inputs.", + "tooltip": "Build mathematical conditions by comparing any two numerical values in your Action logic.", "output": "expression", "message0": "%1", "args0": [ @@ -2581,7 +2581,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_negate 1`] = ` "inputsInline": false, "type": "io_logic_negate", "colour": 60, - "tooltip": "Flip any condition to its opposite - turns true into false and false into true. Essential for creating inverse logic like 'if NOT raining', 'if door is NOT open', or 'if temperature is NOT above 75°F'. Perfect for building exception handling, safety conditions, and reverse automation logic in your IoT Actions.", + "tooltip": "Flip any condition to its opposite - turns true into false and false into true.", "output": "expression", "message0": "not %1", "args0": [ @@ -2601,7 +2601,7 @@ exports[`Block Snapshots > Blockly JSON > io_logic_operation 1`] = ` "inputsInline": true, "type": "io_logic_operation", "colour": 60, - "tooltip": "Combine multiple conditions to create sophisticated decision logic in your Actions. Perfect for complex automation like 'if temperature is high AND humidity is low', 'if motion detected OR door opened', or any scenario where you need multiple criteria to work together. Essential for building smart, multi-factor IoT control systems.", + "tooltip": "Combine multiple conditions to create sophisticated decision logic in your Actions.", "output": "expression", "message0": "%1", "args0": [ @@ -2644,7 +2644,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_arithmetic 1`] = ` "inputsInline": true, "type": "io_math_arithmetic", "colour": 120, - "tooltip": "Perform the specified arithmetic operation on two specified operands.", + "tooltip": "Perform mathematical calculations using sensor data, feed values, or any numbers in your Actions.", "output": "expression", "message0": "%1", "args0": [ @@ -2699,8 +2699,11 @@ exports[`Block Snapshots > Blockly JSON > io_math_constrain 1`] = ` "inputsInline": false, "type": "io_math_constrain", "colour": 120, - "tooltip": "Constrain a given number to fall within a given range.", - "output": "number", + "tooltip": "Keep any number within specified minimum and maximum boundaries.", + "output": [ + "expression", + "number" + ], "message0": "Constrain %1", "args0": [ { @@ -2728,7 +2731,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_number 1`] = ` "inputsInline": false, "type": "io_math_number", "colour": 120, - "tooltip": "A numeric value, whole or decimal.", + "tooltip": "Enter any numerical value for use in your IoT Actions - whole numbers, decimals, positive, or negative.", "output": [ "expression", "number" @@ -2757,7 +2760,7 @@ exports[`Block Snapshots > Blockly JSON > io_math_round 1`] = ` "inputsInline": false, "type": "io_math_round", "colour": 120, - "tooltip": "Round a value to the nearest whole number via round, floor, or ceiling functions", + "tooltip": "Convert decimal numbers to whole numbers using different rounding strategies.", "output": "expression", "message0": "%1 %2", "args0": [ @@ -2795,7 +2798,7 @@ exports[`Block Snapshots > Blockly JSON > io_text 1`] = ` "inputsInline": false, "type": "io_text", "colour": 180, - "tooltip": "A String of text", + "tooltip": "Enter any text content for use in your Actions - words, phrases, device commands, or messages.", "output": [ "expression", "string" @@ -2850,7 +2853,7 @@ exports[`Block Snapshots > Blockly JSON > io_text_multiline 1`] = ` "inputsInline": false, "type": "io_text_multiline", "colour": 180, - "tooltip": "A String of longer-form text with newlines.", + "tooltip": "Create formatted text content with multiple lines, paragraphs, and line breaks.", "output": [ "expression", "string" @@ -2871,12 +2874,402 @@ exports[`Block Snapshots > Blockly JSON > io_text_multiline 1`] = ` } `; +exports[`Block Snapshots > Blockly JSON > io_utility_current_time 1`] = ` +{ + "inputsInline": false, + "type": "io_utility_current_time", + "colour": 360, + "tooltip": "Get the current system time in 24-hour format for use in time comparisons and conditions.", + "output": [ + "expression", + "time" + ], + "message0": "Current Time %1", + "args0": [ + { + "type": "input_dummy", + "align": "RIGHT" + } + ], + "helpUrl": "https://io.adafruit.com/actions-docs/blocks/time/current_time" +} +`; + +exports[`Block Snapshots > Blockly JSON > io_utility_time 1`] = ` +{ + "inputsInline": false, + "type": "io_utility_time", + "colour": 360, + "tooltip": "Create a time value in 24-hour format for use in comparisons and conditions.", + "output": [ + "expression", + "time" + ], + "message0": "%1 : %2 %3", + "args0": [ + { + "name": "HOUR", + "type": "field_dropdown", + "options": [ + [ + "0", + "00" + ], + [ + "1", + "01" + ], + [ + "2", + "02" + ], + [ + "3", + "03" + ], + [ + "4", + "04" + ], + [ + "5", + "05" + ], + [ + "6", + "06" + ], + [ + "7", + "07" + ], + [ + "8", + "08" + ], + [ + "9", + "09" + ], + [ + "10", + "10" + ], + [ + "11", + "11" + ], + [ + "12", + "12" + ], + [ + "13", + "13" + ], + [ + "14", + "14" + ], + [ + "15", + "15" + ], + [ + "16", + "16" + ], + [ + "17", + "17" + ], + [ + "18", + "18" + ], + [ + "19", + "19" + ], + [ + "20", + "20" + ], + [ + "21", + "21" + ], + [ + "22", + "22" + ], + [ + "23", + "23" + ] + ] + }, + { + "name": "MINUTE", + "type": "field_dropdown", + "options": [ + [ + "0", + "00" + ], + [ + "1", + "01" + ], + [ + "2", + "02" + ], + [ + "3", + "03" + ], + [ + "4", + "04" + ], + [ + "5", + "05" + ], + [ + "6", + "06" + ], + [ + "7", + "07" + ], + [ + "8", + "08" + ], + [ + "9", + "09" + ], + [ + "10", + "10" + ], + [ + "11", + "11" + ], + [ + "12", + "12" + ], + [ + "13", + "13" + ], + [ + "14", + "14" + ], + [ + "15", + "15" + ], + [ + "16", + "16" + ], + [ + "17", + "17" + ], + [ + "18", + "18" + ], + [ + "19", + "19" + ], + [ + "20", + "20" + ], + [ + "21", + "21" + ], + [ + "22", + "22" + ], + [ + "23", + "23" + ], + [ + "24", + "24" + ], + [ + "25", + "25" + ], + [ + "26", + "26" + ], + [ + "27", + "27" + ], + [ + "28", + "28" + ], + [ + "29", + "29" + ], + [ + "30", + "30" + ], + [ + "31", + "31" + ], + [ + "32", + "32" + ], + [ + "33", + "33" + ], + [ + "34", + "34" + ], + [ + "35", + "35" + ], + [ + "36", + "36" + ], + [ + "37", + "37" + ], + [ + "38", + "38" + ], + [ + "39", + "39" + ], + [ + "40", + "40" + ], + [ + "41", + "41" + ], + [ + "42", + "42" + ], + [ + "43", + "43" + ], + [ + "44", + "44" + ], + [ + "45", + "45" + ], + [ + "46", + "46" + ], + [ + "47", + "47" + ], + [ + "48", + "48" + ], + [ + "49", + "49" + ], + [ + "50", + "50" + ], + [ + "51", + "51" + ], + [ + "52", + "52" + ], + [ + "53", + "53" + ], + [ + "54", + "54" + ], + [ + "55", + "55" + ], + [ + "56", + "56" + ], + [ + "57", + "57" + ], + [ + "58", + "58" + ], + [ + "59", + "59" + ] + ] + }, + { + "type": "input_dummy", + "align": "RIGHT" + } + ], + "helpUrl": "https://io.adafruit.com/actions-docs/blocks/time/time" +} +`; + exports[`Block Snapshots > Blockly JSON > io_variables_get 1`] = ` { "inputsInline": false, "type": "io_variables_get", "colour": 240, - "tooltip": "Retrieve the value stored in a variable that was previously set using a Set Variable block. Use this to access stored feed values, calculation results, or any data saved earlier in your Action.", + "tooltip": "Retrieve the value stored in a variable that was previously set using a Set Variable block.", "output": "expression", "message0": "Get variable %1 %2", "args0": [ @@ -2898,7 +3291,7 @@ exports[`Block Snapshots > Blockly JSON > io_variables_set 1`] = ` "inputsInline": true, "type": "io_variables_set", "colour": 240, - "tooltip": "Store a value in a named variable for later use in your Action. Variables let you remember feed values, calculation results, or any data to use in subsequent action blocks.", + "tooltip": "Store a value in a named variable for later use in your Action.", "nextStatement": "expression", "previousStatement": "expression", "message0": "Set variable %1 = %2", @@ -2923,7 +3316,7 @@ exports[`Block Snapshots > Blockly JSON > matcher_compare 1`] = ` "inputsInline": true, "type": "matcher_compare", "colour": 224, - "tooltip": "Create smart triggers based on numerical sensor data and thresholds. Perfect for temperature alerts ('notify when above 80°F'), battery monitoring ('warn when below 20%'), humidity control ('turn on fan when over 60%'), or any sensor-based automation that depends on numerical comparisons.", + "tooltip": "Create smart triggers based on numerical sensor data and thresholds.", "output": "matcher", "message0": "%1 %2", "args0": [ @@ -2973,7 +3366,7 @@ exports[`Block Snapshots > Blockly JSON > matcher_text_compare 1`] = ` "inputsInline": true, "type": "matcher_text_compare", "colour": 180, - "tooltip": "Compare text-based feed data using smart text matching. Perfect for triggers based on status messages ('door opened', 'motion detected'), device states ('online', 'offline'), or any text-based sensor data. Works with exact matches, exclusions, or partial text detection within longer messages.", + "tooltip": "Compare text-based feed data using smart text matching.", "output": "matcher", "message0": "%1 %2", "args0": [ @@ -3011,8 +3404,11 @@ exports[`Block Snapshots > Blockly JSON > math_map 1`] = ` "inputsInline": false, "type": "math_map", "colour": 120, - "tooltip": "Scale a value from one range of numbers to another", - "output": "number", + "tooltip": "Transform sensor readings and data values by scaling them from one number range to another.", + "output": [ + "expression", + "number" + ], "message0": "Map %1", "args0": [ { @@ -3092,7 +3488,7 @@ exports[`Block Snapshots > Blockly JSON > minute_settings 1`] = ` "inputsInline": false, "type": "minute_settings", "colour": 30, - "tooltip": "How would you like to specify minutes of the hour for your schedule?", + "tooltip": "How would you like to specify minutes of the hour for your schedule?.", "message0": "Minute: %1", "args0": [ { @@ -3111,7 +3507,7 @@ exports[`Block Snapshots > Blockly JSON > month_settings 1`] = ` "inputsInline": false, "type": "month_settings", "colour": 30, - "tooltip": "How would you like to specify the months portion of your schedule?", + "tooltip": "How would you like to specify the months portion of your schedule?.", "message0": "Month: %1", "args0": [ { @@ -3130,7 +3526,7 @@ exports[`Block Snapshots > Blockly JSON > on_schedule 1`] = ` "inputsInline": false, "type": "on_schedule", "colour": 30, - "tooltip": "Create powerful time-based automation that runs your Actions on a schedule - from simple daily reminders to complex patterns like 'every 15 minutes during weekdays' or 'first Monday of each quarter'. Works like a smart alarm clock for your IoT devices, automatically triggering actions without any manual intervention. Perfect for turning lights on/off, sending regular reports, or controlling devices based on time patterns.", + "tooltip": "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'.", "nextStatement": "trigger", "previousStatement": "trigger", "message0": "Schedule %1", @@ -3886,7 +4282,7 @@ exports[`Block Snapshots > Blockly JSON > text_compare 1`] = ` "inputsInline": true, "type": "text_compare", "colour": 180, - "tooltip": "Compare any two pieces of text or data to build conditional logic in your Actions. Perfect for creating if/then statements like 'if device status equals online', 'if user name is not guest', or 'if error message contains timeout'. Works with feed values, variables, user input, or any text-based data.", + "tooltip": "Compare any two pieces of text or data to build conditional logic in your Actions.", "output": "expression", "message0": "%1", "args0": [ @@ -3933,7 +4329,7 @@ exports[`Block Snapshots > Blockly JSON > text_template 1`] = ` "inputsInline": true, "type": "text_template", "colour": 180, - "tooltip": "Render a text template.", + "tooltip": "Create dynamic, personalized messages by combining static text with live data from your IoT system.", "output": [ "expression", "string" @@ -4109,7 +4505,7 @@ exports[`Block Snapshots > Blockly JSON > when_data 1`] = ` "inputsInline": true, "type": "when_data", "colour": 30, - "tooltip": "The simplest trigger - runs your Action every single time ANY new data arrives at a feed, regardless of what the value is. Perfect for logging all activity ('record every sensor reading'), acknowledging data receipt ('send confirmation for every message'), or triggering workflows that need to process all incoming data. No conditions, no filtering - just pure data arrival detection.", + "tooltip": "The simplest trigger - runs your Action every single time ANY new data arrives at a feed, regardless of what the value is.", "nextStatement": "trigger", "previousStatement": "trigger", "message0": "When %1 gets any data %2", @@ -4142,7 +4538,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching 1`] = ` "inputsInline": true, "type": "when_data_matching", "colour": 30, - "tooltip": "The most common trigger type - runs your Action immediately whenever new data arrives at a feed that meets your specified condition. Perfect for real-time responses like 'send alert when temperature exceeds 85°F', 'turn on lights when motion detected', or 'notify me when battery drops below 20%'. This trigger fires every single time the condition is met.", + "tooltip": "The most common trigger type - runs your Action immediately whenever new data arrives at a feed that meets your specified condition.", "nextStatement": "trigger", "previousStatement": "trigger", "message0": "When %1 gets data matching: %2", @@ -4177,7 +4573,7 @@ exports[`Block Snapshots > Blockly JSON > when_data_matching_state 1`] = ` "inputsInline": true, "type": "when_data_matching_state", "colour": 30, - "tooltip": "Advanced trigger that watches for changes in how your feed data matches a condition over time. Unlike basic triggers that just check if data equals a value, this compares the current data point with the previous one to detect when conditions START being true, STOP being true, or CONTINUE being true. Perfect for detecting state changes like 'temperature just went above 80°' or 'door just closed after being open'.", + "tooltip": "Advanced trigger that watches for changes in how your feed data matches a condition over time.", "nextStatement": "trigger", "previousStatement": "trigger", "message0": "When %1 gets data that %2 matching %3", diff --git a/test/app/blocks/utility_current_time_block_test.js b/test/app/blocks/utility_current_time_block_test.js new file mode 100644 index 0000000..e1c3354 --- /dev/null +++ b/test/app/blocks/utility_current_time_block_test.js @@ -0,0 +1,84 @@ +import { describe, it } from 'node:test' +import { assert } from 'chai' + +import currentTimeBlockDefObject from "#app/blocks/utility/current_time.js" +import BlockDefinition from "#src/definitions/block_definition.js" + + +describe("Utility Current Time Block", () => { + it("works", () => { + const currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + assert.equal(currentTimeDefinition.type, 'io_utility_current_time') + }) + + it("exports block JSON", () => { + const + currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject), + currentTimeBlockJSON = currentTimeDefinition.toBlocklyJSON() + + // contains message and args + assert.exists(currentTimeBlockJSON.message0) + assert.exists(currentTimeBlockJSON.args0) + + // has proper output types + assert.deepEqual(currentTimeBlockJSON.output, ['expression', 'time']) + + // has correct color + assert.equal(currentTimeBlockJSON.colour, 360) + }) + + it("exports instance JSON with correct type", () => { + const + currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject), + currentTimeInstanceJson = currentTimeDefinition.toBlocklyInstanceJSON() + + // has correct type + assert.equal(currentTimeInstanceJson.type, 'io_utility_current_time') + + // no fields needed for current time block + assert.notExists(currentTimeInstanceJson.fields) + }) + + it("generates correct JSON output", () => { + const currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + const [result, precedence] = currentTimeDefinition.generators.json() + const parsedResult = JSON.parse(result) + + assert.equal(precedence, 0) + assert.exists(parsedResult.currentTime) + assert.isObject(parsedResult.currentTime) + }) + + it("regenerates correctly from JSON", () => { + const currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + const blockObject = { + currentTime: {} + } + + const regenerated = currentTimeDefinition.regenerators.json(blockObject) + + assert.equal(regenerated.type, 'io_utility_current_time') + assert.notExists(regenerated.fields) // No fields needed + }) + + it("throws error when regenerating without currentTime data", () => { + const currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + const blockObject = {} + + assert.throws(() => { + currentTimeDefinition.regenerators.json(blockObject) + }, Error, "No currentTime data for io_utility_current_time regenerator") + }) + + it("has compatible output type with time blocks", () => { + const currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + // Should have same output types as regular time blocks for compatibility + const outputTypes = currentTimeDefinition.connections.output + assert.includeMembers(outputTypes, ['expression', 'time']) + }) +}) diff --git a/test/app/blocks/utility_time_block_test.js b/test/app/blocks/utility_time_block_test.js new file mode 100644 index 0000000..51c5b5a --- /dev/null +++ b/test/app/blocks/utility_time_block_test.js @@ -0,0 +1,150 @@ +import { describe, it } from 'node:test' +import { assert } from 'chai' + +import timeBlockDefObject from "#app/blocks/utility/time.js" +import BlockDefinition from "#src/definitions/block_definition.js" + + +describe("Utility Time Block", () => { + it("works", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + assert.equal(timeDefinition.type, 'io_utility_time') + }) + + it("exports block JSON", () => { + const + timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject), + timeBlockJSON = timeDefinition.toBlocklyJSON() + + // contains message and args + assert.exists(timeBlockJSON.message0) + assert.exists(timeBlockJSON.args0) + + // has proper output types + assert.deepEqual(timeBlockJSON.output, ['expression', 'time']) + + // has correct color + assert.equal(timeBlockJSON.colour, 360) + }) + + it("exports instance JSON with correct type", () => { + const + timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject), + timeInstanceJson = timeDefinition.toBlocklyInstanceJSON() + + // has correct type + assert.equal(timeInstanceJson.type, 'io_utility_time') + + // fields are not included in instance JSON by default for dropdown fields + assert.notExists(timeInstanceJson.fields) + }) + + it("generates correct JSON output", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + // Mock block object + const mockBlock = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '14' + if (fieldName === 'MINUTE') return '30' + return null + } + } + + const [result, precedence] = timeDefinition.generators.json(mockBlock) + const parsedResult = JSON.parse(result) + + assert.equal(precedence, 0) + assert.exists(parsedResult.time) + assert.equal(parsedResult.time.display, '14:30') + assert.equal(parsedResult.time.value, 870) // 14 * 60 + 30 = 870 minutes since midnight + }) + + it("regenerates correctly from JSON", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + const blockObject = { + time: { + display: '09:15', + value: 555 // 9 * 60 + 15 = 555 minutes since midnight + } + } + + const regenerated = timeDefinition.regenerators.json(blockObject) + + assert.equal(regenerated.type, 'io_utility_time') + assert.equal(regenerated.fields.HOUR, '09') + assert.equal(regenerated.fields.MINUTE, '15') + }) + + it("handles edge cases correctly", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + // Test midnight + const mockBlockMidnight = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '00' + if (fieldName === 'MINUTE') return '00' + return null + } + } + + const [midnightResult] = timeDefinition.generators.json(mockBlockMidnight) + const parsedMidnight = JSON.parse(midnightResult) + + assert.equal(parsedMidnight.time.display, '00:00') + assert.equal(parsedMidnight.time.value, 0) + + // Test end of day + const mockBlockEndDay = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '23' + if (fieldName === 'MINUTE') return '59' + return null + } + } + + const [endDayResult] = timeDefinition.generators.json(mockBlockEndDay) + const parsedEndDay = JSON.parse(endDayResult) + + assert.equal(parsedEndDay.time.display, '23:59') + assert.equal(parsedEndDay.time.value, 1439) // 23 * 60 + 59 = 1439 minutes since midnight + }) + + it("integrates correctly with comparison blocks", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + // Test that time blocks can be compared with each other + const mockBlockA = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '09' + if (fieldName === 'MINUTE') return '30' + return null + } + } + + const mockBlockB = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '17' + if (fieldName === 'MINUTE') return '45' + return null + } + } + + const [resultA] = timeDefinition.generators.json(mockBlockA) + const [resultB] = timeDefinition.generators.json(mockBlockB) + + const parsedA = JSON.parse(resultA) + const parsedB = JSON.parse(resultB) + + // Verify that the numeric values can be compared + assert.isTrue(parsedA.time.value < parsedB.time.value, '09:30 should be less than 17:45') + assert.equal(parsedA.time.value, 570) // 9 * 60 + 30 + assert.equal(parsedB.time.value, 1065) // 17 * 60 + 45 + + // Verify display formats are correct + assert.equal(parsedA.time.display, '09:30') + assert.equal(parsedB.time.display, '17:45') + }) +}) \ No newline at end of file diff --git a/test/app/blocks/utility_time_integration_test.js b/test/app/blocks/utility_time_integration_test.js new file mode 100644 index 0000000..736a073 --- /dev/null +++ b/test/app/blocks/utility_time_integration_test.js @@ -0,0 +1,168 @@ +import { describe, it } from 'node:test' +import { assert } from 'chai' + +import timeBlockDefObject from "#app/blocks/utility/time.js" +import currentTimeBlockDefObject from "#app/blocks/utility/current_time.js" +import compareBlockDefObject from "#app/blocks/math/compare.js" +import BlockDefinition from "#src/definitions/block_definition.js" + + +describe("Time Blocks Integration", () => { + it("Time and Current Time blocks have compatible output types", () => { + const + timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject), + currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + // Both should output the same types for compatibility + assert.deepEqual(timeDefinition.connections.output, currentTimeDefinition.connections.output) + assert.includeMembers(timeDefinition.connections.output, ['expression', 'time']) + }) + + it("Time blocks can be used with comparison blocks", () => { + const + timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject), + currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject), + compareDefinition = BlockDefinition.parseRawDefinition(compareBlockDefObject) + + // Verify that time blocks output 'expression' type which compare block accepts + assert.include(timeDefinition.connections.output, 'expression') + assert.include(currentTimeDefinition.connections.output, 'expression') + + // Compare block should accept 'expression' inputs + assert.equal(compareDefinition.inputs.A.check, 'expression') + assert.equal(compareDefinition.inputs.B.check, 'expression') + }) + + it("generates JSON that can be compared mathematically", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + // Create mock blocks for different times + const mockEarlyTime = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '09' + if (fieldName === 'MINUTE') return '00' + return null + } + } + + const mockLateTime = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '17' + if (fieldName === 'MINUTE') return '30' + return null + } + } + + const [earlyResult] = timeDefinition.generators.json(mockEarlyTime) + const [lateResult] = timeDefinition.generators.json(mockLateTime) + + const parsedEarly = JSON.parse(earlyResult) + const parsedLate = JSON.parse(lateResult) + + // Verify mathematical comparison works + assert.isTrue(parsedEarly.time.value < parsedLate.time.value, '09:00 should be less than 17:30') + assert.equal(parsedEarly.time.value, 540) // 9 * 60 = 540 + assert.equal(parsedLate.time.value, 1050) // 17 * 60 + 30 = 1050 + }) + + it("Current Time block generates compatible JSON structure", () => { + const currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + const [result] = currentTimeDefinition.generators.json() + const parsedResult = JSON.parse(result) + + // Should have currentTime object (structure will be handled by backend) + assert.exists(parsedResult.currentTime) + assert.isObject(parsedResult.currentTime) + }) + + it("supports common time comparison scenarios", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + // Business hours scenario: 9 AM to 5 PM + const startHours = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '09' + if (fieldName === 'MINUTE') return '00' + return null + } + } + + const endHours = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '17' + if (fieldName === 'MINUTE') return '00' + return null + } + } + + const lunch = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '12' + if (fieldName === 'MINUTE') return '00' + return null + } + } + + const [startResult] = timeDefinition.generators.json(startHours) + const [endResult] = timeDefinition.generators.json(endHours) + const [lunchResult] = timeDefinition.generators.json(lunch) + + const startTime = JSON.parse(startResult).time.value + const endTime = JSON.parse(endResult).time.value + const lunchTime = JSON.parse(lunchResult).time.value + + // Verify logical time ordering + assert.isTrue(startTime < lunchTime, 'Start time should be before lunch') + assert.isTrue(lunchTime < endTime, 'Lunch should be before end time') + assert.isTrue(startTime < endTime, 'Start should be before end') + + // Verify specific values for common times + assert.equal(startTime, 540) // 9:00 = 9 * 60 + assert.equal(lunchTime, 720) // 12:00 = 12 * 60 + assert.equal(endTime, 1020) // 17:00 = 17 * 60 + }) + + it("handles edge cases correctly", () => { + const timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject) + + // Midnight + const midnight = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '00' + if (fieldName === 'MINUTE') return '00' + return null + } + } + + // End of day + const endOfDay = { + getFieldValue: (fieldName) => { + if (fieldName === 'HOUR') return '23' + if (fieldName === 'MINUTE') return '59' + return null + } + } + + const [midnightResult] = timeDefinition.generators.json(midnight) + const [endOfDayResult] = timeDefinition.generators.json(endOfDay) + + const midnightValue = JSON.parse(midnightResult).time.value + const endOfDayValue = JSON.parse(endOfDayResult).time.value + + // Verify edge case values + assert.equal(midnightValue, 0, 'Midnight should be 0') + assert.equal(endOfDayValue, 1439, 'End of day should be 1439 minutes') + assert.isTrue(midnightValue < endOfDayValue, 'Midnight should be less than end of day') + }) + + it("both blocks belong to the same category", () => { + const + timeDefinition = BlockDefinition.parseRawDefinition(timeBlockDefObject), + currentTimeDefinition = BlockDefinition.parseRawDefinition(currentTimeBlockDefObject) + + // Both blocks should be in Time category and have same color + assert.equal(timeDefinition.color, 360) + assert.equal(currentTimeDefinition.color, 360) + }) +}) \ No newline at end of file