Test Bundle (DO NOT INSTALL) [2.0.0.M1;2.0.0.M3)

Warning! This topic is for development testing only. Do not install, it will probably cause issues with your system.

version: 1
blocks:
  astro_newyaml_marketplace:
    tags:
      - Test
    props:
      parameters: []
      parameterGroups: []
    component: BlockLibrary
    config:
      name: Astro Marketplace
    slots:
      blocks:
        - component: BlockType
          config:
            type: compare_event_time
            message0: the %1 of today's %2 according to %3 %4
            args0:
              - type: field_dropdown
                name: MOMENT
                options:
                  - - start
                    - START
                  - - end
                    - END
              - type: field_dropdown
                name: PHASE
                options:
                  - - sunrise
                    - SUN_RISE
                  - - sunset
                    - SUN_SET
                  - - astro dawn
                    - ASTRO_DAWN
                  - - nautic dawn
                    - NAUTIC_DAWN
                  - - civil dawn
                    - CIVIL_DAWN
                  - - astro dusk
                    - ASTRO_DUSK
                  - - nautic dusk
                    - NAUTIC_DUSK
                  - - civil dusk
                    - CIVIL_DUSK
                  - - daylight
                    - DAYLIGHT
                  - - noon
                    - NOON
                  - - night
                    - NIGHT
                  - - morning night
                    - MORNING_NIGHT
                  - - evening night
                    - EVENING_NIGHT
              - type: input_value
                name: THING
              - type: field_dropdown
                name: DIRECTION
                options:
                  - - is still to come
                    - After
                  - - has already passed
                    - Before
            output: Boolean
            colour: 0
            tooltip: ""
            helpUrl: ""
          slots:
            code:
              - component: BlockCodeTemplate
                config:
                  template: "{{utility:things}}.getActions('astro', {{input:THING}}).getEventTime('{{field:PHASE}}', null, '{{field:MOMENT}}').is{{field:DIRECTION}}({{utility:zdt}}.now())"
            toolbox:
              - component: PresetInput
                config:
                  name: THING
                  type: oh_thing
                  shadow: true
        - component: BlockType
          config:
            type: time_until_event
            message0: number of %1 until the %2 of today's %3 according to %4
            args0:
              - type: field_dropdown
                name: UNIT
                options:
                  - - seconds
                    - SECONDS
                  - - minutes
                    - MINUTES
                  - - hours
                    - HOURS
              - type: field_dropdown
                name: MOMENT
                options:
                  - - start
                    - START
                  - - end
                    - END
              - type: field_dropdown
                name: PHASE
                options:
                  - - sunrise
                    - SUN_RISE
                  - - sunset
                    - SUN_SET
                  - - astro dawn
                    - ASTRO_DAWN
                  - - nautic dawn
                    - NAUTIC_DAWN
                  - - civil dawn
                    - CIVIL_DAWN
                  - - astro dusk
                    - ASTRO_DUSK
                  - - nautic dusk
                    - NAUTIC_DUSK
                  - - civil dusk
                    - CIVIL_DUSK
                  - - daylight
                    - DAYLIGHT
                  - - noon
                    - NOON
                  - - night
                    - NIGHT
                  - - morning night
                    - MORNING_NIGHT
                  - - evening night
                    - EVENING_NIGHT
              - type: input_value
                name: THING
            output: Boolean
            colour: 0
            tooltip: Determines the amount of time until the specified event. If it has already passed, the result will be negative.
            helpUrl: ""
          slots:
            code:
              - component: BlockCodeTemplate
                config:
                  template: "{{utility:zdt}}.now().until({{utility:things}}.getActions('astro', {{input:THING}}).getEventTime('{{field:PHASE}}', null, '{{field:MOMENT}}'), {{utility:ChronoUnit}}.{{field:UNIT}})"
            toolbox:
              - component: PresetInput
                config:
                  name: THING
                  type: oh_thing
                  shadow: true
      utilities:
        - component: UtilityJavaType
          config:
            name: things
            javaClass: org.openhab.core.model.script.actions.Things
        - component: UtilityJavaType
          config:
            name: zdt
            javaClass: java.time.ZonedDateTime
        - component: UtilityJavaType
          config:
            name: ChronoUnit
            javaClass: java.time.temporal.ChronoUnit
version: 1
widgets:
  colorbands_gauge_newyaml:
    tags:
      - gauge
      - marketplace:161215
      - colorbands
    props:
      parameters:
        - context: item
          description: The Item
          label: Item
          name: item
          required: true
          type: TEXT
          groupName: gaugeConfig
        - default: "0"
          description: The minimum value to show
          label: Min Value
          name: min
          required: false
          type: TEXT
          groupName: gaugeConfig
        - default: "100"
          description: The maximum value to show
          label: Max Value
          name: max
          required: false
          type: TEXT
          groupName: gaugeConfig
        - description: The title text
          label: Title
          name: title
          required: false
          type: TEXT
          groupName: appearance
        - default: 18
          description: The size of the title font
          label: Title Font Size
          name: titleFontSize
          required: false
          type: INTEGER
          groupName: appearance
          advanced: true
        - default: 0
          description: The number of digits after the decimal separator
          label: Precision
          name: precision
          required: false
          type: INTEGER
          min: 0
          max: 4
          groupName: appearance
        - default: true
          description: Whether to print the unit on the dial
          label: Show Unit
          name: showUnit
          required: false
          type: BOOLEAN
          groupName: appearance
        - default: 5
          description: The number of splits
          label: Splits
          name: splits
          required: false
          type: INTEGER
          min: 1
          max: 20
          groupName: appearance
        - default: 3
          description: The number of ticks between each split
          label: Ticks
          name: ticks
          required: false
          type: INTEGER
          min: 1
          max: 20
          groupName: appearance
        - default: "#e64a19"
          description: The needle color
          label: Needle Color
          name: needleColor
          required: false
          type: TEXT
          groupName: appearance
        - default: "#ddd"
          description: The gauge frame color
          label: Frame Color
          name: frameColor
          required: false
          type: TEXT
          groupName: appearance
        - default: transparent
          description: The dial color
          label: Dial Color
          name: dialColor
          required: false
          type: TEXT
          groupName: appearance
        - default: "#464646"
          description: The dial print color
          label: Print Color
          name: printColor
          required: false
          type: TEXT
          groupName: appearance
        - default: transparent
          description: The shadow color, e.g. "black" or "#0008"
          label: Shadow Color
          name: shadowColor
          required: false
          type: TEXT
          groupName: appearance
          advanced: true
        - default: 10
          description: The blur value for the dial shadow (no effect if the dial is transparent)
          label: Shadow Blur
          name: shadowBlur
          required: false
          type: INTEGER
          groupName: appearance
          advanced: true
        - default: 3
          description: The offset value for the dial shadow (no effect if the dial is transparent)
          label: Shadow Offset
          name: shadowOffset
          required: false
          type: INTEGER
          groupName: appearance
          advanced: true
        - default: 15
          description: The axis font size
          label: AxisFontSize
          name: axisFontSize
          required: false
          type: INTEGER
          min: 6
          max: 50
          groupName: appearance
          advanced: true
        - default: false
          label: Enable LCD
          name: enableLCD
          required: false
          type: BOOLEAN
          groupName: lcd
        - default: 0
          description: The number of digits after the decimal separator
          label: LCD Precision
          name: lcdPrecision
          required: false
          type: INTEGER
          min: 0
          max: 4
          groupName: lcd
        - description: The height of widget
          label: Height
          name: height
          required: false
          type: TEXT
          groupName: appearance
        - default: 0.08
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 1 End
          name: colorBand1End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#039be688"
          description: The color value for the band
          label: Color Band 1 Color
          name: colorBand1Color
          required: false
          type: TEXT
          groupName: bands
        - default: 0.6
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 2 End
          name: colorBand2End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#43a04788"
          description: The color value for the band
          label: Color Band 2 Color
          name: colorBand2Color
          required: false
          type: TEXT
          groupName: bands
        - default: 0.8
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 3 End
          name: colorBand3End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#ffa60088"
          description: The color value for the band
          label: Color Band 3 Color
          name: colorBand3Color
          required: false
          type: TEXT
          groupName: bands
        - default: 1
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 4 End
          name: colorBand4End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#db443788"
          description: The color value for the band
          label: Color Band 4 Color
          name: colorBand4Color
          required: false
          type: TEXT
          groupName: bands
        - default: 1
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 5 End
          name: colorBand5End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#db443788"
          description: The color value for the band
          label: Color Band 5 Color
          name: colorBand5Color
          required: false
          type: TEXT
          groupName: bands
        - default: 1
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 6 End
          name: colorBand6End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#db443788"
          description: The color value for the band
          label: Color Band 6 Color
          name: colorBand6Color
          required: false
          type: TEXT
          groupName: bands
        - default: 1
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 7 End
          name: colorBand7End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#db443788"
          description: The color value for the band
          label: Color Band 7 Color
          name: colorBand7Color
          required: false
          type: TEXT
          groupName: bands
        - default: 1
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 8 End
          name: colorBand8End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#db443788"
          description: The color value for the band
          label: Color Band 8 Color
          name: colorBand8Color
          required: false
          type: TEXT
          groupName: bands
        - default: 1
          description: The decimal value between 0 and 1 that determines when the color band ends
          label: Color Band 9 End
          name: colorBand9End
          required: false
          type: DECIMAL
          groupName: bands
        - default: "#db443788"
          description: The color value for the band
          label: Color Band 9 Color
          name: colorBand9Color
          required: false
          type: TEXT
          groupName: bands
      parameterGroups:
        - name: gaugeConfig
          label: Gauge Configuration
        - name: appearance
          label: Appearance
        - name: lcd
          label: LCD
        - name: bands
          label: Color bands
        - name: clickAction
          context: action
          label: Click Action
          description: Action to execute if the widget it clicked. Disables tooltips.
    component: f7-block
    config:
      class:
        - no-margin
    slots:
      default:
        - component: oh-link
          config:
            actionPropsParameterGroup: clickAction
            style:
              display: =(props.click_action&&props.click_action!='false')?'block':'none'
              height: 100%
              position: absolute
              width: 100%
        - component: oh-chart
          config:
            height: =props.height
          slots:
            series:
              - component: oh-data-series
                config:
                  emptyCircleStyle:
                    color: =props.dialColor
                    shadowBlur: =props.shadowBlur
                    shadowColor: =props.shadowColor
                    shadowOffsetX: =props.shadowOffset
                    shadowOffsetY: =props.shadowOffset
                  radius: 85%
                  type: pie
              - component: oh-data-series
                config:
                  anchor:
                    itemStyle:
                      color: "#000"
                    offsetCenter:
                      - 0
                      - 35.5%
                    show: true
                    size: 20
                  axisLabel:
                    show: false
                  axisLine:
                    lineStyle:
                      color:
                        - - 1
                          - =props.frameColor
                      shadowBlur: 6
                      shadowColor: "#0008"
                      width: 6
                  axisTick:
                    show: false
                  data:
                    - name: =items[props.item].state.split(' ')[1]
                  detail:
                    show: false
                  endAngle: -270
                  pointer:
                    show: false
                  radius: 85%
                  splitLine:
                    show: false
                  startAngle: 90
                  title:
                    color: =props.printColor
                    fontWeight: 600
                    offsetCenter:
                      - 0
                      - -23%
                    show: =props.showUnit && items[props.item].state.split(' ').length > 1
                  type: gauge
              - component: oh-data-series
                config:
                  anchor:
                    itemStyle:
                      color: =props.needleColor
                      shadowBlur: 0
                    show: true
                    showAbove: true
                    size: 16
                  axisLabel:
                    color: =props.printColor
                    distance: 20
                    fontSize: =props.axisFontSize
                    formatter: =v=>v.toFixed(props.precision)
                  axisLine:
                    lineStyle:
                      color:
                        - - =props.colorBand1End
                          - =props.colorBand1Color
                        - - =props.colorBand2End
                          - =props.colorBand2Color
                        - - =props.colorBand3End
                          - =props.colorBand3Color
                        - - =props.colorBand4End
                          - =props.colorBand4Color
                        - - =props.colorBand5End
                          - =props.colorBand5Color
                        - - =props.colorBand6End
                          - =props.colorBand6Color
                        - - =props.colorBand7End
                          - =props.colorBand7Color
                        - - =props.colorBand8End
                          - =props.colorBand8Color
                        - - =props.colorBand9End
                          - =props.colorBand9Color
                      width: 15
                  axisTick:
                    distance: -15
                    length: 15
                    lineStyle:
                      color: =props.printColor
                    splitNumber: =props.ticks
                  center:
                    - 50%
                    - 65%
                  clockwise: true
                  data:
                    - itemStyle:
                        color: =props.needleColor
                      name: =props.title
                      value: =Number.parseFloat(items[props.item].state)
                  detail:
                    backgroundColor: rgb(137,150,96)
                    borderColor: "#000"
                    borderWidth: 1
                    color: "#000"
                    fontFamily: Courier New
                    fontSize: 1em
                    fontWeight: 600
                    formatter: "=v=>v.toFixed(props.lcdPrecision)+(items[props.item].state.split(' ').length > 1 ? ' '+items[props.item].state.split(' ')[1] : '')"
                    offsetCenter:
                      - 0
                      - 25%
                    padding:
                      - 0
                      - 2
                      - 0
                      - 2
                    show: =props.enableLCD?true:false
                    valueAnimation: true
                    width: 50%
                  endAngle: 45
                  max: =Number.parseFloat(props.max)
                  min: =Number.parseFloat(props.min)
                  pointer:
                    icon: path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z
                    itemStyle:
                      color: =props.needleColor
                      shadowBlur: 2
                      shadowColor: "#000a"
                      shadowOffsetX: 2
                      shadowOffsetY: 2
                    keepAspect: false
                    length: 105%
                    offsetCenter:
                      - 0
                      - 10%
                  radius: 90%
                  splitLine:
                    distance: -15
                    length: 20
                    lineStyle:
                      color: =props.printColor
                      width: 2
                  splitNumber: =props.splits
                  startAngle: 135
                  title:
                    color: =props.printColor
                    fontSize: =props.titleFontSize
                    offsetCenter:
                      - 0
                      - -30%
                  tooltip:
                    formatter: "=props.title ? p=>props.title+': <b>'+p.value.toFixed(props.precision)+(items[props.item].state.split(' ').length > 1 ? ' '+items[props.item].state.split(' ')[1] : '')+'</b>' : NULL"
                    valueFormatter: "=v=>v.toFixed(props.precision)+(items[props.item].state.split(' ').length > 1 ? ' '+items[props.item].state.split(' ')[1] : '')"
                  type: gauge
            tooltip:
              - component: oh-chart-tooltip
                config:
                  show: true
version: 1
ruleTemplates:
  rules_tools:open_reminder_mp:
    label: Open Reminder Marketplace
    tags:
      - marketplace:138199
    description: This will trigger on a change to an Item in a Group. If the change is different from a "good state" (or the same as the "good state" if the match is inverted) and it remains that state for a specified amount of time it will call another script and reschedule to repeat until the Item returns or a do not distrurb period is entered.
    configDescriptions:
      groupTriggers:
        context: item
        description: Group whose members should trigger this rule.
        label: Group of Triggering Items
        required: true
        type: TEXT
        filterCriteria:
          - value: Group
            name: type
      alertState:
        description: Any state different from this will cause the rule to run.
        label: Alert State
        required: true
        type: TEXT
      invert:
        description: Inverts the match so an state that is the same as Alert State will cause the rule to run.
        label: Invert
        type: BOOLEAN
        default: "false"
      defaultTimeout:
        description: "If a triggering Item doesn't have a timeout metadata, the default initial time to wait before calling the script the first time. Use \"PTVdWhXmYsZz\" format where d=days, h=hours, m=minutes, s=seconds, and z=milliseconds (e.g. \"PT2d5s\" = two days, five seconds)."
        label: Default Initial Timeout
        required: true
        type: TEXT
        default: PT15m
      timeoutMetadata:
        description: "Item metadata namespace to check for a custom initial timeout for the triggering Item. Use \"PTVdWhXmYsZz\" format where d=days, h=hours, m=minutes, s=seconds, and z=milliseconds (e.g. \"PT2d5s\" = two days, five seconds)."
        label: Default Item Initial Timeout Metadata
        type: TEXT
        default: rem_time
      repeatPeriod:
        description: "How often to repeat calling the alert rule after the initial call while the Item remains in a \"not good\" state.  Use \"PTVdWhXmYsZz\" format where d=days, h=hours, m=minutes, s=seconds, and z=milliseconds (e.g. \"PT2d5s\" = two days, five seconds). If left blank no repeat is performed."
        label: Repeat Period
        type: TEXT
        default: ""
      reschedule:
        description: "Reschedule the initial looping timer when the Item changes to the alerting state again or ignore the state change. Put another way, set the timer to run after the first alert event, or after the last alert event. Usually you'll make this true for motion sensor rules."
        label: Reschedule
        type: BOOLEAN
        default: "false"
      alertRuleUID:
        context: rule
        description: Rule called after the initial timeout and every repeat period thereafter as long as the Item remains in a "not good" state.
        label: Alert Rule
        type: TEXT
      dndStart:
        context: time
        description: "The start of the time where alerts are not allowed. Alerts are only sent outside of the time between this time and the alerting end time. If both are the same, alerts will be sent any time."
        label: Do Not Distrub Start Time
        type: TEXT
        default: 00:00
      dndEnd:
        context: time
        description: "The end of the time where alerts are not allowed. Alerts are only sent outside of the alerting start time and alerting end time. If both are the same, alerts will be send any time."
        label: Do Not Disturb End Time
        type: TEXT
        default: 00:00
    actions:
      - id: "2"
        config:
          type: application/javascript;version=ECMAScript-2021
          script: |
            var {loopingTimer, timeUtils} = require('openhab_rules_tools');
            var logger = log('rules_tools.'+ruleUID);

            // Rule Properties
            var alertState = "{{alertState}}";
            var defaultTimeout = "{{defaultTimeout}}";
            var timeoutMetadata = "{{timeoutMetadata}}";
            var alertRuleUID = "{{alertRuleUID}}";
            var repeatPeriod = "{{repeatPeriod}}";
            var reschedule = {{reschedule}};
            var dndStart = "{{dndStart}}";
            var dndEnd = "{{dndEnd}}";
            var noAlertPeriod = (dndStart == dndEnd);
            var isAlertState = (curr) => { return ({{invert}}) ? (curr == alertState) : (curr != alertState); };

            var TIMERS_KEY = ruleUID+'_timers';
            var TIMER_BUFFER = 600*1000000; // Nano seconds

            // If you edit this rule, run it manually to clear out the cache or else
            // errors will occur complaining about the context being closed.
            if(this.event === undefined) {
              logger.info('Resetting looping timers');
              cache.put(TIMERS_KEY, null);
            }
            else {
              const timers = cache.get(TIMERS_KEY, () => new Map());
              const item = event.itemName;
              const state = event.itemState.toString();

              // Temporary work around until PR to fix openhab-js gets merged
              const isBetweenTimes = function (t, start, end) {
                const startTime = time.toZDT(start).toLocalTime();
                const endTime = time.toZDT(end).toLocalTime();
            //    const currTime = this.toLocalTime();
                const currTime = time.toZDT(t).toLocalTime();

                // time range spans midnight
                if (endTime.isBefore(startTime)) {
                  return currTime.isAfter(startTime) || currTime.isBefore(endTime);
                } else {
                  return currTime.isAfter(startTime) && currTime.isBefore(endTime);
                }
              };

              // Looks at the currently scheduled timers and makes the time for a new one
              // doesn't overlap.
              // Loop through the timers and check the timeout time against the existing timers.
              // If it's within half a second of one, move the timer forward 500 msec.
              // If the timeout was moved at least once, loop through the timers again to make
              // sure we didn't move it on top of an already checked timer.
              const avoidOverlapping = (timeout) => {
                let rval = time.toZDT(timeout);
                let recheck = true;
                while(recheck) {
                  recheck = false;
                  logger.debug('There are ' + timers.size + ' timers already scheduled');
                  timers.forEach((value, key) => {
                    if(!value.hasTerminated() &&
                      rval.isClose(time.toZDT(value.timer.getExecutionTime()), time.Duration.ofMillis(250))) {
                      logger.debug('Found timer ' + value.name + ' that has not terminated and is too close to timeout');
                      rval = rval.plusNanos(TIMER_BUFFER);
                      recheck = true;
                    }
                  });
                }
                return rval;
              };

              // Moves the alert time to the end of the DND period if the timeout is
              // in the DND period. Then it makes sure it's at least TIMER_BUFFER nanosecs
              // from any existing timer
              const generateAlertTime = (timeout, dnds, dnde) => {
                let rval = time.toZDT(timeout);
                if(isBetweenTimes(rval, dnds, dnde)) { // temporary work around until PR gets merged to openhab-js
                  logger.debug('Timer is scheduled during DND, moving to end of DND');
                  rval = dndEnd;
                  if(time.toZDT(dndEnd).isBefore(time.toZDT())) {
                    rval = timeUtils.toTomorrow(dndEnd);
                  }
                }
                return avoidOverlapping(rval);
              };

              // This returns the function called by the LoopingTimer. LoopingTimer will
              // reschedule the timer based on what the function passed to it returns. If
              // a duration is returned it will be reschuled for that time into the future.
              // If null is returned the looping stops.
              const sendAlertGenerator = (alertItem, repeat, dnds, dnde) => {
                return () => {
                  const currState = items.getItem(alertItem).state;
                  logger.debug('Timer expired for ' + alertItem + ' with dnd between ' + dnds + ' and ' + dnde);

                  // Calculate the next repeat time
                  let repeatTime = (repeat) ? generateAlertTime(repeat, dnds, dnde) : null;

                  // Send alert
                  if(isAlertState(currState)) {
                    logger.debug('Item is still in an alert state.')
                    rules.runRule(alertRuleUID, { 'alertItem': alertItem, 'currState': currState }, true);
                  }
                  // Cancel the repeats
                  else {
                    logger.debug('Item has returned to the non-alert state');
                    repeatTime = null;
                  }

                  // If there is no repeat delete the timer from the map
                  if(!repeatTime) {
                    logger.debug('No repeat time');
                    timers.delete(alertItem);
                  }

                  logger.debug('Rescheduling for ' + repeatTime);
                  return repeatTime;
                };
              };


              logger.debug('Running door alert rule with: \n' +
                           '  item:           ' + item + '\n' +
                           '  state:          ' + state + '\n' +
                           '  alertState:     ' + alertState + '\n' +
                           '  invert:         ' + true + '\n' +
                           '  defaultTimeout: ' + defaultTimeout + '\n' +
                           '  repeatPeriod:   ' + repeatPeriod + '\n' +
                           '  dndStart:       ' + dndStart + '\n' +
                           '  dndEnd:         ' + dndEnd + '\n' +
                           '  noAlertPeriod:  ' + noAlertPeriod + '\n' +
                           '  alertRuleUID:   ' + alertRuleUID);

              // Returned to alertState and timer exists, cancel the looping timer
              if(!isAlertState(state) && timers.has(item)) {

                logger.debug(item + ' has returned to a non-alerting state of ' + state + ', canceling the timer if it exists.');
                timers.get(item).cancel();
                timers.delete(item);

              }

              // Item changed to a not alertState, create a looping timer if one doesn't already exist
              else if(isAlertState(state)) {
                logger.debug(item + ' is in the alert state of ' + state);

                // There shouldn't be a Timer if the Item just changed to the alertState, log to show
                // something went wrong.
                if(timers.has(item)) {
                  logger.warn(item + ' state is now ' + state + ' but an alert timer already exists! This should not have happened!');
                }
                // Schedule a looping timer to start at the initial timeout (from metadata) and repeat
                // on repeatPeriod
                else {
                  const metadata = items.getItem(item).getMetadataValue(timeoutMetadata);
                  let timeout = generateAlertTime((metadata) ? metadata : defaultTimeout, dndStart, dndEnd);
                  timers.set(item, new loopingTimer.LoopingTimer()); // add name as argument when openhab_rules_tools is updated to support that
                  logger.debug('Starting timer for ' + item + ' with ' + timeout);
                  timers.get(item).loop(sendAlertGenerator(item, repeatPeriod, dndStart, dndEnd), timeout);
                }
              }
            }
        type: Script
    triggers:
      - id: "1"
        config:
          groupName: "{{groupTriggers}}"
        type: MemberChanged
Version: 0.0.1
DependsOn: ui-basic
Keywords: test, debug
Countries: ; NO, SE,  
License: GPL
Connection: local
LoggerPackages: com.example.test
Documentation: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/blob/master/bundles/org.openhab.binding.milllan/README.md
Issues: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/issues
Version: 0.0.2
Keywords: test, debug, testing
Countries: DE
CoreRange: [3.1.0:5)
Maturity: immature
Documentation: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/blob/master/README.md
Description: An even more unstable version
LoggerPackages: com.example.test2
URL: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/releases/download/v1.0.0/org.openhab.binding.milllan-1.0.0.jar
[
  {
    "actions": [
      {
        "configuration": {
          "script": "// Version 1.0\nvar {TimerMgr, helpers} = require('openhab_rules_tools');\nconsole.loggerName = 'org.openhab.automation.rules_tools.TimeStateMachine';\n//osgi.getService('org.apache.karaf.log.core.LogService').setLevel(console.loggerName, 'DEBUG');\n\nhelpers.validateLibraries('4.2.0', '2.0.3');\n\nconsole.debug('Starting state machine in ten seconds...');\n\n// Properties\nvar STATE_ITEM  = \"{{timeOfDay}}\";\nvar DT_GROUP    = \"{{timesOfDayGrp}}\";\nvar DAY_TYPES   = ['custom', 'holiday', 'dayset', 'weekend', 'weekday', 'default'];\nvar NAMESPACE   = '{{namespace}}';\nvar USAGE =   'Time Based State Machine Usage:\\n'\n            + 'All date times must be a member of ' + DT_GROUP + '.\\n'\n            + 'Each member of the Group must have ' + NAMESPACE + ' Item metadata of the following format:\\n'\n            + '  .items file: ' + NAMESPACE +'=\"STATE\"[type=\"daytype\", set=\"dayset\", file=\"uri\"]\\n'\n            + \"  UI YAML: use '\" + NAMESPACE + \"' for the namespace and metadata format:\\n\"\n            + '    value: STATE\\n'\n            + '    config:\\n'\n            + '      type: daytype\\n'\n            + '      set: dayset\\n'\n            + '      file: uri\\n'\n            + 'Where \"STATE\" is the state machine state that begins at the time stored in that Item, '\n            + '\"daytype\" is one of \"default\", \"weekday\", \"weekend\", \"dayset\", \"holiday\", or \"custom\". '\n            + 'If \"dayset\" is chosen for the type, the \"set\" property is required indicating the name of the '\n            + 'custom dayset configured in Ephemeris. If \"custom\" is chosen as the type, the \"file\" property '\n            + 'is required and should be the fully qualified path the the Ephemeris XML file with the custom '\n            + 'holidays defined. The \"set\" and \"file\" properties are invalid when choosing any of the other '\n            + '\"types\".';\n\n/**\n * Validates the passed in Item has valid NAMESPACE metadata.\n *\n * @param {string} itemName name of the Item to check\n * @throws exception if the metadata doesn't exist or is invalid\n */\nvar validateItemConfig = (itemName) => {\n  const md = items[itemName].getMetadata()[NAMESPACE];\n\n  if(md.value === undefined || md.value === null || md.value === '') {\n    throw itemName + ' has malformed ' + NAMESPACE + ' metadata, no value found!';\n  }\n\n  const dayType = md.configuration['type'];\n  if(!dayType) {\n    throw itemName + ' has malformed ' + NAMESPACE + ' metadata, required \"type\" property is not found!';\n  }\n\n  if(dayType == 'dayset' && !md.configuration['set']) {\n    throw itemName + ' has malformed ' + NAMESPACE + ' metadata, type is \"dayset\" but required \"set\" property is not found!';\n  }\n\n  if(dayType == 'custom' && !md.configuration['file']) {\n    throw itemName + ' has malformed ' + NAMESPACE + ' metadata, type is \"custom\" but required \"file\" property is not found!';\n  }\n\n  if(!items[itemName].type.startsWith('DateTime')) {\n    throw itemName + ' is not a DateTime Item!';\n  }\n\n  if(items[itemName].isUninitialized) {\n    throw itemName + \" is not initialized!: \" + items[itemName].state;\n  }\n\n  console.debug(itemName+ ' is valid');\n};\n\n/**\n * Return all members of the DT_GROUP that has a \"type\" metadata configuration property that\n * matches the passed in type.\n *\n * @param {string} type the day type defined in the metadata we want to get the Items for\n * @returns {Array} all the Items with the matching type in the metadata\n */\nvar getItemsOfType = (type) => {\n  const allItems = items[DT_GROUP].members;\n  return allItems.filter( item => item.getMetadata()[NAMESPACE].configuration['type'] == type);\n};\n\n/**\n * Returns true if all the Items of the given type have a unique \"state\" value\n * in the metadata.\n *\n * @param {string} the day type\n * @returns {boolean} true if all states are unique, false otherwise\n */\nvar checkUniqueStates = (type) => {\n  const allItems = getItemsOfType(type);\n  const states = new Set(allItems.map(i => { return i.getMetadata()[NAMESPACE].value; }));\n  return !allItems.length || allItems.length == states.size;\n};\n\n/**\n * Check that all Items are configured correctly.\n */\nvar validateAllConfigs = () => {\n  console.debug('Validating Item types, Item metadata, and Group membership');\n\n  // Check that all members of the Group have metadata\n  const itemsWithMD = items[DT_GROUP].members.filter(item => item.getMetadata(NAMESPACE)).length;\n  if(itemsWithMD != items[DT_GROUP].members.length) {\n    const noMdItems = items[DT_GROUP].members.filter(item => !item.getMetadata(NAMESPACE));\n    console.warn('The following Items do not have required ' + NAMESPACE + ' metadata: ' + noMdItems.map(item => item.name).join(', '));\n    return false; // no sense on performing any additional tests\n  }\n\n  // Check each Item's metadata\n  let isGood = helpers.checkGrpAndMetadata(NAMESPACE, DT_GROUP, validateItemConfig, USAGE);\n\n  // Check the state item\n  if(!items[STATE_ITEM]){\n    console.warn('The state Item ' + STATE_ITEM + ' does not exist!');\n    isGood = false;\n  }\n\n  if(!items[STATE_ITEM].type.startsWith('String')) {\n    console.warn('The state Item ' + STATE_ITEM + ' is not a String Item!');\n    isGood = false;\n  }\n\n  // Check to see if we have a default set of Items\n  if(!getItemsOfType('default')) {\n    console.warn('There are no \"default\" day type Items defined! Make sure you have all day types covered!');\n    // we'll not invalidate if there are no \"default\" items\n  }\n\n  // Check that each data set has a unique state for each Item\n  DAY_TYPES.forEach(type => {\n    if(!checkUniqueStates(type)) {\n      console.warn('Not all the metadata values for Items of type ' + type + ' are unique!');\n      isGood = false;\n    }\n  })\n\n  // Report if all configs are good or not\n  if(isGood) {\n    console.debug('All ' + NAMESPACE + ' Items are configured correctly');\n  }\n  return isGood;\n};\n\n/**\n * Pull the set of Items for today based on Ephemeris. The Ephemeris hierarchy is\n * - custom\n * - holiday\n * - dayset\n * - weeekend\n * - weekday\n * - default\n *\n * If there are no DateTime Items defined for today's type, null is returned.\n */\nvar getTodayItems = () => {\n  // Get all the DateTime Items that might apply to today given what type of day it is\n  // For example, if it's a weekend, there will be no weekday Items pulled. Whether or not\n  // the entry in this dict has an array of Items determines whether today is of that day\n  // type.\n  const startTimes = [\n    { 'type': 'custom',  'times' : getItemsOfType('custom').filter(item => actions.Ephemeris.isBankHoliday(0, item.getMetadata()[NAMESPACE].configuration['file'])) },\n    { 'type': 'holiday', 'times' : (actions.Ephemeris.isBankHoliday()) ? getItemsOfType('holiday') : [] },\n    { 'type': 'dayset',  'times' : getItemsOfType('dayset').filter(item => actions.Ephemeris.isInDayset(items.getMetadata()[NAMESPACE].configuration['set'])) },\n    { 'type': 'weekend', 'times' : (actions.Ephemeris.isWeekend()) ? getItemsOfType('weekend') : [] },\n    { 'type': 'weekday', 'times' : (!actions.Ephemeris.isWeekend()) ? getItemsOfType('weekday') : [] },\n    { 'type': 'default', 'times' : getItemsOfType('default') }\n  ];\n\n  // Go through startTimes in order and choose the first one that has a non-empty list of Items\n  const dayStartTimes = startTimes.find(dayset => dayset.times.length);\n\n  if(dayStartTimes === null) {\n    console.warn('No DateTime Items found for today');\n    return null;\n  }\n  else {\n    console.info('Today is a ' + dayStartTimes.type + ' day.');\n    return dayStartTimes.times;\n  }\n};\n\n/**\n * Returns a function called to transition the state machine from one state to the next\n *\n * @param {string} state the new state to transition to\n * @param {function} the function that transitions the state\n */\nvar stateTransitionGenerator = (state) => {\n  return function() {\n    console.info('Transitioning Time State Machine from ' + items[STATE_ITEM].state + ' to ' + state);\n    items[STATE_ITEM].sendCommand(state);\n  }\n}\n\n/**\n * Returns a function that generates the timers for all the passed in startTimes\n *\n * @param {Array} startTimes list of today's state start times\n * @param {timerMgr.TimerMgr} timers collection of timers\n * @returns {function} called to generate the timers to transition between the states\n */\nvar createTimersGenerator = (timers) => {\n  return function() {\n\n    if(validateAllConfigs()) {\n\n      // Cancel the timers, skipping the debounce timer\n      console.debug('Cancelling existing timers');\n      timers.cancelAll();\n\n      // Get the set of Items for today's state machine\n      console.debug(\"Acquiring today's state start times\");\n      const startTimes = getTodayItems();\n\n      // Get the state and start time, sort them ignoring the date, skip the ones that have\n      // already passed and create a timer to transition for the rest.\n      console.debug('Creating timers for times that have not already passed');\n      var mapped = startTimes.map(i => { return { 'state': i.getMetadata()[NAMESPACE].value,\n                                                  'time' : time.toZDT(i.state).toToday() } });\n      mapped.sort((a,b) => {\n               if(a.time.isBefore(b.time)) return -1;\n                else if(a.time.isAfter(b.time)) return 1;\n                else return 0;\n              })\n              .filter(tod => tod.time.isAfter(time.toZDT()))\n              .forEach(tod => {\n                // TODO: see if we can move to rules instead of timers\n                console.debug('Creating timer for ' + tod.state + ' at ' + tod.time);\n                timers.check(tod.state, tod.time.toString(), stateTransitionGenerator(tod.state));\n              });\n\n      // Figure out the current time of day and move to that state if necessary\n      var beforeTimes = mapped.sort((a,b) => {\n                                if(a.time.isAfter(b.time)) return -1;\n                                else if(a.time.isBefore(b.time)) return 1;\n                                else return 0;\n                              })\n                              .filter(tod => tod.time.isBefore(time.toZDT()));\n      if(!beforeTimes.length) {\n        console.debug(\"There is no date time for today before now, we can't know what the current state is, keeping the current time of day state of \" + items[STATE_ITEM].state + \".\");\n      }\n      else {\n        const currState = beforeTimes[0].state\n        const stateItem = items[STATE_ITEM];\n        console.info('The current state is ' + currState);\n        if(stateItem.state != currState) stateItem.sendCommand(currState)\n      }\n    }\n    else {\n      console.warn('The config is not valid, cannot proceed!');\n    }\n\n  };\n};\n\nvar timers = cache.private.get('timers', () => TimerMgr());\n\n// Wait a minute after the last time the rule is triggered to make sure all Items are done changing (e.g.\n// Astro Items) before calculating the new state.\ntimers.check('debounce',\n             'PT10S',\n             createTimersGenerator(timers),\n             true,\n             () => { console.debug('Flapping detected, waiting before creating timers for today'); });\n",
          "type": "application/javascript"
        },
        "id": "3",
        "inputs": {
        },
        "type": "script.ScriptAction"
      }
    ],
    "conditions": [
    ],
    "configDescriptions": [
      {
        "context": "item",
        "description": "String Item that holds the current time of day's state.",
        "filterCriteria": [
          {
            "name": "type",
            "value": "String"
          }
        ],
        "label": "Time of Day State Item",
        "name": "timeOfDay",
        "required": true,
        "type": "TEXT"
      },
      {
        "context": "item",
        "description": "Has as members all the DateTime Items that define time of day states.",
        "filterCriteria": [
          {
            "name": "type",
            "value": "Group"
          }
        ],
        "label": "Times of Day Group",
        "name": "timesOfDayGrp",
        "required": true,
        "type": "TEXT"
      },
      {
        "description": "The Item metadata namespace (e.g. \"tsm\").",
        "label": "Time of Day Namespace",
        "name": "namespace",
        "required": true,
        "type": "TEXT"
      }
    ],
    "description": "Creates timers to transition a state Item to a new state at defined times of day.",
    "label": "Time Based State Machine4",
    "triggers": [
      {
        "configuration": {
          "groupName": "{{timesOfDayGrp}}"
        },
        "id": "1",
        "type": "core.GroupStateChangeTrigger"
      },
      {
        "configuration": {
          "startlevel": 100
        },
        "id": "2",
        "type": "core.SystemStartlevelTrigger"
      },
      {
        "configuration": {
          "time": "00:05"
        },
        "id": "4",
        "type": "timer.TimeOfDayTrigger"
      }
    ],
    "uid": "rules_tools:tsm_m"
  },
  {
    "actions": [
      {
        "configuration": {
          "script": "var from = parseFloat(oldState.toString().split(' ')[0]);\nvar to = parseFloat(newState.toString().split(' ')[0]);\n\nprint(from + '>' + to);\n\nif (to < {{threshold}} && from >= {{threshold}}) {\n  events.sendCommand('{{alertItem}}', '{{alertCommand}}');\n}\n",
          "type": "application/javascript"
        },
        "id": "2",
        "inputs": {
        },
        "type": "script.ScriptAction"
      }
    ],
    "conditions": [
    ],
    "configDescriptions": [
      {
        "context": "item",
        "description": "Item that holds the power (in watts) of the washing machine. Can be a quantity type (Number:Power).",
        "label": "Power Item",
        "name": "powerItem",
        "required": true,
        "type": "TEXT"
      },
      {
        "defaultValue": 2,
        "description": "When the power measurement was at or above the threshold and crosses below it, trigger the alert.",
        "label": "Threshold",
        "name": "threshold",
        "required": true,
        "type": "DECIMAL"
      },
      {
        "context": "item",
        "description": "Item to send a command to when the measured power gets below the threshold. For instance, a Hue light advanced Alert channel.",
        "label": "Alert Item",
        "name": "alertItem",
        "required": true,
        "type": "TEXT"
      },
      {
        "defaultValue": "LSELECT",
        "description": "Command to send to the alert item (for an item linked to a Hue light alert channel, LSELECT will flash the light for a few seconds).",
        "label": "Alert Command",
        "name": "alertCommand",
        "required": true,
        "type": "TEXT"
      }
    ],
    "description": "This will monitor the power consumption of a washing machine and send an alert command when it gets below a threshold, meaning it has finished.",
    "label": "Alert when Washing Machine Finished2",
    "triggers": [
      {
        "configuration": {
          "itemName": "{{powerItem}}",
          "state": ""
        },
        "id": "1",
        "type": "core.ItemStateChangeTrigger"
      }
    ],
    "uid": "ysc:washing_machine_alert_m"
  }
]
Version: 0.0.2-alpha
Keywords: test, debug, testing
Countries: DE, CZ
CoreRange: [3.1.0:5)
DependsOn:
Maturity: alpha
Documentation: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/blob/master/README.md
Description: An extremely unstable version
LoggerPackages: com.example.test2a
URL: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/releases/download/v1.0.1/org.openhab.binding.milllan-1.0.1.jar
some:alpha:json
Version: 0.0.3
CoreRange: [4.2.2:6)
uid: test_widget
props:
  parameterGroups: []
  parameters:
    - name: prop1
      label: Prop 1
      type: TEXT
      description: A text prop
    - name: item
      label: Item
      type: TEXT
      context: item
      description: An item to control
tags: []
component: f7-card
config:
  title: '=(props.item) ? "State of " + props.item : "Set props to test!"'
  footer: =props.prop1
  content: =items[props.item].displayState || items[props.item].state
Version: 0.0.4.beta1
Maturity: beta
coreRange: [4.2..5.1]
Description: The old tried and tested version
Issues: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/blob/master/bundles/org.openhab.binding.milllan/README.md
BLOCKURL: https://raw.githubusercontent.com/deibich/openhab-libraries/23565b891b4399aaca3e878071dc963b43673d19/blockly/libraries/datetime/datetime.yaml
URL: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/releases/download/v1.0.0/org.openhab.binding.milllan-1.0.0.jar
RULEURL: https://raw.githubusercontent.com/mherwege/openhab-rule-templates/refs/heads/main/wasteBelgium/wasteBelgium-ES6.yaml
Countries: UA
uid: config:dsl:doNothing
label: Does Nothing
type: DSL
configuration:
  function: >
    logInfo("transform", "this transform does nothing")
    input
Version: 0.0.5.alpha
Maturity: beta
coreRange: [4.2..5.2]
Description: The latest hype
Issues: https://github.com/Nadahar/Mill-LAN-openHAB-Binding/blob/master/bundles/org.openhab.binding.milllan/README.md
Countries: UA, ES
uid: config:dsl:doNothing
label: Still Does Nothing
type: DSL
configuration:
  function: >
    logInfo("transform", "this transform does nothing either")
    input