Advanced scheduling widget

Hello everybody.

I want to create a widget for a kind of scheduler, but I don’t even know how to approach it.

I have a mqtt device which reads from a topic a string with the format “hh:mm,hh:mm…”, hh:mm being times of the day, e.g. “08:00,21:00”. I would like to have a widget like some kind of list, where the user can click a plus button or something, add a new element to the list (ideally with a time widget), and remove an element from the list with a trash can icon or a minus symbol or something like that. The end result should be the “compiled” string with all the elements of the list sent via mqtt.

First of all, I don’t know if that’s even possible. All widgets seem to be just for “reading” data or executing predefined actions, but nothing dynamic like this.

I thought of having a group item of string items and add and remove elements from it, but I would need to create items dynamically, which doesn’t look possible.

Maybe using advanced scripts can help, but I have to idea how. Every thing I think seems to need dynamically created items.

I even asked chatgpt for ideas, but of course it just gave me some example full with made up functions which isn’t even a valid yaml.

I wish I could paste some previous attempts, but as I said, I’m completely at lost.

Any help, even if it’s just a suggestion of an approach, would be greatly appreciated .

I’m traveling right now so I can’t leave to detailed a response at the moment, but this is quite doable, and doesn’t even require multiple items.

If I were to tackle this, I would probably start with a single item that holds the full schedule string. Then I would use a repeater to make the list by splitting the string item state into an array. That gets you the basic display.

To edit the array is a little more difficult, but I would have a rule associated with the widget that does one thing: receives the edited time value and array location of an element and changes the item that holds the array string. When you use the rule action from a widget component you can also add variables to the rule’s context, so that’s how you pass the relevant information to the rule when it runs.

Thanks for the help, and sorry for the delay.

Your idea works, but it is a bit more complicated than I expected.

Here’s an attempt of a widget, still not fully working. I don’t understand these f7 things, sometimes they show items, sometimes they don’t, it’s a bit confusing:

    - component: oh-list-card
      config:
        mediaList: true
        title: =items["WateringSystem_PumpSchedule"]
      slots:
        default:
        - component: oh-repeater
          config:
            fragment: true
            for: item
            sourceType: array
            in: =items["WateringSystem_PumpSchedule"].state.split(",")
          slots:
            default:
              #- component: oh-list-item
              #  config:
              #    title: =loop.item
              #    icon: f7:clock
              - component: f7-list
                config:
                  #footer: =items["PumpScheduleItem"].state.split(",")[loop.int_idx]
                  visible: true
                slots:
                  default:
                    - component: f7-list-item
                      config:
                        style:
                          box-shadow: red
                        title: =loop.item
                        #icon: '=props.PumpScheduleItem.state == loop.int_idx) ? "f7:checkmark_alt_circle_fill" : "f7:circle"'
                        icon: f7:clock
                        #title: =loop.item
                        #badge: =items["lTop_Str"].state.split(";")[loop.int_idx]
                        #badgeColor: =items["lCol_Str"].state.split(";")[loop.int_idx]
                        #listButton: false
                        #action: command
                        #actionItem: lDel_Num
                        #actionCommand: '=(loop.int_idx == "") ? "0" : loop.int_idx'
                      slots:
                        content:
                          - component: oh-button
                            config:
                              text: =loop.item
                              action: command
                              actionItem: WateringSystem_PumpControl
                              actionCommand: ON
                              style:
                                width: 100%
                                margin-top: 10px

Note the amount of commented lines in my attempts.

However, now I’m focusing on creating the edit widget. I took a time picker widget as a base, and I got to some working state. I still have to add a button that will call the rule (it doesn’t looks complicated). My current problem with this widget is that I’m trying to disable the buttons when it’s out of range, but when I go down, it loops to the initial value. e.g., the value of the item is the string “09:40”. If I click the decrement button on the hour, it will work correctly, but when it reaches “1” (an it shows “1” instead of “01”, the next time I click it goes back to “09” (and this time it’s padded with the 0), and it keeps going back to 08, 07, etc.

uid: timepicker
tags: []
props:
  parameters:
    - context: item
      description: Item to control
      label: Item
      name: item
      required: true
      type: TEXT
  parameterGroups: []
timestamp: Jun 30, 2024, 7:17:14 PM
component: f7-block
config:
  label: Time picker
slots:
  default:
    - component: f7-row
      config:
        variables:
          hours: 0
          minutes: 0
        class:
          - margin
      slots:
        default:
          - component: f7-col
            config:
              width: "'100'"
            slots:
              default:
                - component: f7-row
                  config: {}
                  slots:
                    default:
                      - component: f7-col
                        config:
                          width: "45"
                          style:
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                disabled: "=(vars.hours || Number.parseInt(items[props.item].state.split(':')[0])) >= Number.parseInt('23') ? true : false"
                                action: variable
                                actionVariable: hours
                                actionVariableValue: =((vars.hours || Number.parseInt(items[props.item].state.split(':')[0])) + 1)
                                text: ▲
                                style:
                                  font-family: u2400
                                  font-size: 150%
                                  --f7-button-text-color: var(--f7-text-color)
                      - component: f7-col
                        config:
                          width: "10"
                      - component: f7-col
                        config:
                          width: "45"
                          style:
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                disabled: "=(vars.minutes || Number.parseInt(items[props.item].state.split(':')[1])) >= Number.parseInt('59') ? true : false"
                                action: variable
                                actionVariable: minutes
                                actionVariableValue: =((vars.minutes || Number.parseInt(items[props.item].state.split(':')[1])) + 1)
                                text: ▲
                                style:
                                  font-family: u2400
                                  font-size: 150%
                                  --f7-button-text-color: var(--f7-text-color)
                - component: f7-row
                  config: {}
                  slots:
                    default:
                      - component: f7-col
                        config:
                          width: "45"
                        slots:
                          default:
                            - component: oh-label-card
                              config:
                                variable: hours
                                label: =(vars.hours ||(('0' + Number.parseInt(items[props.item].state.split(':')[0])).slice(-2)))
                      - component: f7-col
                        config:
                          width: "10"
                          style:
                            margin: auto
                            text-align: center
                        slots:
                          default:
                            - component: Label
                              config:
                                style:
                                  font-size: 200%
                                text: ":"
                      - component: f7-col
                        config:
                          width: "45"
                        slots:
                          default:
                            - component: oh-label-card
                              config:
                                variable: minutes
                                label: =(vars.minutes ||(('0' + Number.parseInt(items[props.item].state.split(':')[1])).slice(-2)))
                - component: f7-row
                  config: {}
                  slots:
                    default:
                      - component: f7-col
                        config:
                          width: "45"
                          style:
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                disabled: "=(vars.hours || Number.parseInt(items[props.item].state.split(':')[0])) <= Number.parseInt('0') ? true : false"
                                action: variable
                                actionVariable: hours
                                actionVariableValue: =((vars.hours || Number.parseInt(items[props.item].state.split(':')[0])) - 1)
                                text: ▼
                                style:
                                  font-family: u2400
                                  font-size: 150%
                                  --f7-button-text-color: var(--f7-text-color)
                      - component: f7-col
                        config:
                          width: "10"
                      - component: f7-col
                        config:
                          width: "45"
                          style:
                            text-align: center
                        slots:
                          default:
                            - component: oh-button
                              config:
                                disabled: "=(vars.minutes || Number.parseInt(items[props.item].state.split(':')[1])) <= Number.parseInt('0') ? true : false"
                                action: variable
                                actionVariable: minutes
                                actionVariableValue: =((vars.minutes || Number.parseInt(items[props.item].state.split(':')[1])) - 1)
                                text: ▼
                                style:
                                  font-family: u2400
                                  font-size: 150%
                                  --f7-button-text-color: var(--f7-text-color)

This is a great start. It’s pretty much exactly what I was thinking. You may or may not want the mediaList property in the end. It will depend on what you do with list items themselves.

You get a little off track here. You’re already inside a list. That’s what the list card does for you. You don’t want to nest lists like this, you just want to start adding list items directly to the list card’s list. So the list item itself should be the direct child of the repeater.

The f7-list-item has gotten you into a little bit of trouble here. You don’t see the clock icon in your list because the f7 component doesn’t take an icon property, that’s added in by the oh version, the oh-list-item. It also looks like you tried to add an action to the list item, but that didn’t work for the same reason. Actions are not an f7 feature they are an oh specific feature so they will only work with some oh elements. So here, it seems you really want to use the oh-list-item instead of the f7-list-item.

Once you can add the action directly to the oh-list-item you probably won’t even need the button any more, but if you’d rather have the action isolated to the button area instead of the entire list item then you can still do this, you’ll just want to use the after slot instead of the content slot. That will give you much better visual results.

As for the time picker widget, the sort of expression you want in the disabled property is a slightly tricky one. There are several issues with this:

and some of them are the reason that you are getting the strange behavior with the numbers.

  1. You don’t need the ternary statement here. The ternary syntax (test) ? return value if true : return value if false should be used when you need expression results that are other than true and false. But, only needs a boolean value, so you just need the test part which, by definition returns a true or false value.
  2. Number.parseInt('23') is necessary. That just converts the string literal '23' into a numeric type, but you can use directly use 23 instead which is a numeric type by default. You see parseInt or parseFloat applied often in widget expressions, but that’s because the state of an item is always returned as a string, even if it is a numerical item. In older OH versions that was the only return option for items, so most numerical item states need to be cast to a number type. If you’re just using, numbers, just type the number.
  3. The last issue is with the test expression itself. You’ve followed the standard suggestion to test that the variable of interest is defined or use a backup default value, which is good. The problem is that your variable can also hold a falsey value : 0. When you roll the number down to 0, that counts as a false value so (vars.hours || Number.parseInt(items[props.item].state.split(':')[0])) returns the item state instead of 0.

There are a few different ways to fix this issue. If you are on a new enough version of OH (snapshot from within the last few weeks, or the new milestone) then you can use the new oh-context component to define your variables with default values. The avoids that entire problem with the undefined variable test and your disabled expression would just be:

disabled: =vars.hours <= 0

If you want this widget to be usable for older versions of OH then you do still have to wrestle the possibility of undefined variables so you need to test for that explicitly instead of using the OR shortcut. This is where you can use the ternary expression because you want (if hours variable is not undefined) ? return hours variable : return item state and then that return value is used in the numerical comparison.

disabled: =((vars.hours !== undefined)?(vars.hours):(Number.parseInt(items[props.item].state.split(':')[0]))) <= 0

That will mostly work, but we’re going to have revise it again in just a moment because of another version of the same 0 issue.

If you run with that disabled expression and decrease the hour variable, when you get to 1 and then decrease again, instead of 0 you’re going to get false or - or something like that. Why? because, again, 0 is a falsey value so when your decrease button calculates the value to send and gets 0 the yaml interprets any false value as that key not existing. So instead of 0 there’s no variable value sent.

There’s no great way around this because it’s just a combination of how yaml works and how the OH expressions work. What I usually do is actually convert the variable values to strings by appending .toString().

But, of course, if you do that, then in any area where you want to do a calculation or a test with the variable, you need to reconvert it back into a number. So going back to that previous solution as one example, we can just move parseInt from around the item state to around the entire ternary expression because both the possible return values are strings:

disabled: =Number.parseInt((vars.hours !== undefined)?(vars.hours):(items[props.item].state.split(':')[0])) <= 0

I did some advances in the timepicker widget, and I also did the working script. I didn’t touch the main widget, but I don’t think it will be too hard. Here’s my code:

uid: timepicker
tags: []
props:
  parameters:
    - context: item
      description: Item to control
      label: Item
      name: item
      required: true
      type: TEXT
    - description: index
      label: Index
      name: index
      required: true
      type: INTEGER
  parameterGroups: []
timestamp: Jul 7, 2024, 5:30:29 PM
component: oh-context
config:
  variables:
    hours: =(Number.parseInt(items[props.item].state.split(':')[0]))
    minutes: =(Number.parseInt(items[props.item].state.split(':')[1]))
slots:
  default:
    - component: f7-block
      config:
        label: Time picker
      slots:
        default:
          - component: f7-row
            config:
              class:
                - margin
            slots:
              default:
                - component: f7-col
                  config:
                    width: 50px
                    style:
                      #--f7-cols-per-row: 3
                      #--f7-grid-gap: 100px
                  slots:
                    default:
                      - component: f7-row
                        config: {}
                        slots:
                          default:
                            - component: f7-col
                              config:
                                style:
                                  text-align: center
                                width: "15"
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      action: variable
                                      actionVariable: hours
                                      actionVariableValue: =(vars.hours+1) % 24
                                      style:
                                        --f7-button-text-color: var(--f7-text-color)
                                        font-family: u2400
                                        font-size: 150%
                                      text: ▲
                            - component: f7-col
                              config:
                                width: "10"
                            - component: f7-col
                              config:
                                style:
                                  text-align: center
                                width: "15"
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      action: variable
                                      actionVariable: minutes
                                      actionVariableValue: =(vars.minutes+1) % 60
                                      style:
                                        --f7-button-text-color: var(--f7-text-color)
                                        font-family: u2400
                                        font-size: 150%
                                      text: ▲
                      - component: f7-row
                        config: {}
                        slots:
                          default:
                            - component: f7-col
                              config:
                                width: "15"
                              slots:
                                default:
                                  - component: oh-label-card
                                    config:
                                      label: =('0' + vars.hours).slice(-2)
                                      variable: hours
                            - component: f7-col
                              config:
                                style:
                                  margin: auto
                                  text-align: center
                                width: "10"
                              slots:
                                default:
                                  - component: Label
                                    config:
                                      style:
                                        font-size: 200%
                                      text: ":"
                            - component: f7-col
                              config:
                                width: "15"
                              slots:
                                default:
                                  - component: oh-label-card
                                    config:
                                      label: =('0' + vars.minutes).slice(-2)
                                      variable: minutes
                            - component: f7-col
                              config:
                                width: "15"
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      iconF7: plus
                                      action: rule
                                      actionRule: water_system_schedule_control
                                      actionRuleContext:
                                        hours: =vars.hours
                                        minutes: =vars.minutes
                                        index: =props.index
                                        remove: false
                            - component: f7-col
                              config:
                                width: "15"
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      iconF7: minus
                                      action: rule
                                      actionRule: water_system_schedule_control
                                      actionRuleContext:
                                        remove: true
                                        index: =props.index
                      - component: f7-row
                        config: {}
                        slots:
                          default:
                            - component: f7-col
                              config:
                                style:
                                  text-align: center
                                width: "15"
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      action: variable
                                      actionVariable: hours
                                      actionVariableValue: =(((vars.hours-1) % 24) + 24 ) % 24
                                      style:
                                        --f7-button-text-color: var(--f7-text-color)
                                        font-family: u2400
                                        font-size: 150%
                                      text: ▼
                            - component: f7-col
                              config:
                                width: "10"
                            - component: f7-col
                              config:
                                style:
                                  text-align: center
                                width: "15"
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      action: variable
                                      actionVariable: minutes
                                      actionVariableValue: =(((vars.minutes-1) % 60) + 60 ) % 60
                                      style:
                                        --f7-button-text-color: var(--f7-text-color)
                                        font-family: u2400
                                        font-size: 150%
                                      text: ▼

This is obviously work in project, but I have some ugly visual problems:
image

Forget temporarily the fact that in the 1st and 3rd rows are only 3 columns and in the middle one 5. The main problem is that the 3 columns try to expand themselves to use the whole width. I just want to have them close together, like the normal oh-input widget for time should be it if worked with numbers or strings (actually I would like to use simply an oh-input widget, but it doesn’t suit my usecase).

Another problem is that there aren oh-button-card for some reason, so I cannot center those buttons properly.

It is not the columns, it is the row the columns are in. The default flex spacing of an f7-row is “space-between” which will cause the row to grow to its maximum width (usually the width of its container) and the elements inside it to be spread across that entire width. You want to set the rows justify-content style to “center” to get all the column elements close together in the middle.

If you are not familiar with flexbox styling, then I suggest this reference:

Because a label card can just act as a button (i.e., you can give the label card all the same action you can give a button), there’s no sense in having a separate oh-button-card. If you want the “card” look then just use and oh-label-card instead of the buttons.

Positioning those elements will be the same either way. You’ll need to understand what the parent elements are doing (the row and column elements) and how the buttons interact with that.

Thanks for you answers.

The oh-label-card instead of oh-button seems to work.

However, I’ve tried the justify-content thing, and it works in the first and last rows (the ones with the arrow buttons), but not on the middle one, maybe because it uses cards?

Sorry, I meant to mention that in the last reply. The middle row does have one element that is expanding to fill all the available space which is why it looks like justify-content doesn’t work. Because you have set the margin to “auto” on the one column, that column has additional margin width added to it until it fills the whole width. You’ll need to not set the whole margin, but just set margin-top and margin-bottom instead.