Presence - Card from ScrewPlate

Here is a widget with which the presence and absence (also the duration of the absence) can be displayed.

ScrewPlate will be a template set with various widgets, all of which are highly customizable in a unified style. More information on GitHub

Since version 1.2, you no longer need to use an extra rule. You just need to create an item (DateTime) with a profile (timestamp of the last change) and select it to display the time of absence.

Deprecated:
I use the following DSL rule to calculate the duration of absence.

if (Handy_Online.state == OFF) { val lastUpdate1 = Handy_Online.lastUpdate val awaySince1 = lastUpdate1.until(now, java.time.temporal.ChronoUnit.MINUTES) VI_Handy_AwaySince.postUpdate(awaySince1) logInfo(“Presence”,"AwaySince: "+ awaySince1) } else { VI_Handy_AwaySince.postUpdate(0) }

Screenshots

Changelog

In each widget there is a tag with version, please always make sure to use the latest version.

Change and further information on GitHub

Resources

https://raw.githubusercontent.com/DrScr3w/ScrewPlate/refs/heads/main/templates/Card_Template_Presence.yaml

2 Likes

You could use an expression in the widget for this instead of the rule.

component: oh-label-card
config:
  item: MotionSensor_LastMotion
  label: =dayjs(items.MotionSensor_LastMotion.state).fromNow()
  title: Dad's Last Motion Detection

Is there a way to change the colour of the presence-icons due to state? Would’ve loved it, if the icon was red for away and green for present?

Anywhere you see a color element there is already an expression. For example:

                                  color: "=props.contentIconColor ? props.contentIconColor : (themeOptions.dark
                                    === 'dark' ? 'white' : 'black')"

You can replace that expression with one that tests the presence of the person.

                                  color: "=items[props.person1item].state == 'ON') ? 'green': 'red'" 

I have added the option.

1 Like

When I use it, it always shows me “a month ago.”

And what’s the full data time? It uses both the date and time portion of the timestamp. If you are only updating the time it won’t show anything more recent than that date.

I have a DateTime item with the current value “2025-10-23 15:27:13”.

:person_shrugging: All I can say is what I posted continues to work for me.

This is a DateTime Item? Not a String? That doesn’t look like the normal formatting for a DateTime Item’s state which should look like “2025-10-23T15:27:13.123-0200”.

dayjs expects an ISO8601 DateTime String.

When I create an item with the last change, I get the value mentioned above.

What does that profile do?

What is the actual state of the Item? Meaning what do you see in events.log? What do you see in Settings → Items? What do you see if you log out the state of the Item in a rule?

The thing comes from the network binding and the item represents the ON/OFF status of the cell phone. In addition, there is an item in the timestamp of the last change with the above-mentioned profile.

So, in the widget with

=dayjs(items.Handy_Online_LastUpdate.state).fromNow()

the difference since the last change should be displayed, at least that’s what I thought.

I can also see the correct format in the log.

Item ‘Handy_Online_lastUpdate’ shall update to 2025-10-23T18:32:46.402221322+0200

Sorry, it was my mistake, I must have had tomatoes on my eyes.

=dayjs(items.[props.person1ItemLastUpdate].state).fromNow()

Can dayjs still be formatted in this case?

.format(???)

I think you need to get rid of the . after items.

=dayjs(items[props.person1ItemLastUpdate].state).fromNow()

items is a map with the item name as the key.

I’m not sure I understand the question. fromNow() does format it as a phrase (e.g. “five minutes ago”). I’m not sure you have any additional formatting control over that though. There is a link to the dayjs docs on the expressions page in the OH docs which will tell you everything you can do with it.

Thanks, it’s already been taken care of.

Since widget version 1.2, you no longer need to use an extra rule. You just need to create an item (DateTime) with a profile (timestamp of the last change) and select it to display the time of absence.

Hi @DrScr3w,

thanks for this nice template.

I made a change to it that makes it easier to adopt the widget to a different number of persons - using the array definition method from Garbage Collection - comma separated lists.

uid: Card_Template_Presence
tags:
  - Card Template Presence
  - Customizable Template
  - ScrewPlate
  - Version 1.3
props:
  parameters:
    - description: "Input without 'px' (Default: none)"
      label: Card Height
      name: cardHeight
      required: false
      type: TEXT
      groupName: cardGroup
    - description: "Input without 'px' (Default: 20px)"
      label: Card Border Radius
      name: cardBorderRadius
      required: false
      type: TEXT
      groupName: cardGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Card Border Color
      name: cardBorderColor
      required: false
      type: TEXT
      groupName: cardGroup
    - description: "Input without 'px' (Default: 3px)"
      label: Card Border Size
      name: cardBorderSize
      required: false
      type: TEXT
      groupName: cardGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Card Background Color
      name: cardBgColor
      required: false
      type: TEXT
      groupName: cardGroup
    - default: "false"
      label: Card Background Image - Show on Card
      name: cardBgImageShow
      required: false
      type: BOOLEAN
      groupName: cardGroup
    - description: Input file name for Background Image (uploaded to /html/ folder)
      label: Card Background Image
      name: cardBgImage
      required: false
      type: TEXT
      groupName: cardGroup
    - default: "false"
      label: Icon - Show on Card
      name: iconShow
      required: false
      type: BOOLEAN
      groupName: iconGroup
    - description: Use oh:iconName (openHAB icon), f7:iconName (Framework7 icon),
        material:iconName (Material icon) or iconify:iconSet:iconName
      label: Icon
      name: iconName
      required: false
      type: TEXT
      groupName: iconGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Icon Color
      name: iconColor
      required: false
      type: TEXT
      groupName: iconGroup
    - description: "Input without 'px' (Default: 30px)"
      label: Icon  Size
      name: iconSize
      required: false
      type: TEXT
      groupName: iconGroup
    - default: "false"
      label: Header - Show on Card
      name: headerShow
      required: false
      type: BOOLEAN
      groupName: headerGroup
    - description: Input Text to diplay on Header
      label: Header
      name: headerText
      required: false
      type: TEXT
      groupName: headerGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Header Text Color
      name: headerTextColor
      required: false
      type: TEXT
      groupName: headerGroup
    - description: "Input without 'px' (Default: 25px)"
      label: Header Text Size
      name: headerTextSize
      required: false
      type: TEXT
      groupName: headerGroup
    - default: "false"
      label: Footer - Show on Card
      name: footerShow
      required: false
      type: BOOLEAN
      groupName: footerGroup
    - description: Input Text to diplay on Footer
      label: Footer
      name: footerText
      required: false
      type: TEXT
      groupName: footerGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Footer Text Color
      name: footerTextColor
      required: false
      type: TEXT
      groupName: footerGroup
    - description: "Input without 'px' (Default: 16px)"
      label: Footer Text Size
      name: footerTextSize
      required: false
      type: TEXT
      groupName: footerGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Content Text Color
      name: contentTextColor
      required: false
      type: TEXT
      groupName: contentGroup
    - description: "Input without 'px' (Default: 18px)"
      label: Content Text Size
      name: contentTextSize
      required: false
      type: TEXT
      groupName: contentGroup
    - description: "Input without 'px' (Default: 30px)"
      label: Content Icon Size
      name: contentIconSize
      required: false
      type: TEXT
      groupName: contentGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Content Icon Color
      name: contentIconColor
      required: false
      type: TEXT
      groupName: contentGroup
    - description: "Input text value for present (Default: present)"
      label: Present Text Value
      name: presentTextValue
      required: false
      type: TEXT
      groupName: customGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Present Icon Color (only usable if the icon is transparent)
      name: presentIconColor
      required: false
      type: TEXT
      groupName: customGroup
    - description: "Input text value for absence (Default: absent)"
      label: Absence Text Value
      name: absenceTextValue
      required: false
      type: TEXT
      groupName: customGroup
    - description: Input like '#FFFFFF' or 'red', 'yellow' etc.
      label: Absence Icon Color (only usable if the icon is transparent)
      name: absenceIconColor
      required: false
      type: TEXT
      groupName: customGroup
    - default: "false"
      description: Display your own labels for the people
      label: Person Label - Show on Card
      name: personLabelShow
      required: false
      type: BOOLEAN
      groupName: customGroup
    - description: Labels for persons - to display Name; comma separated list
      label: Person Labels List
      name: personLabelArray
      required: false
      type: TEXT
      groupName: customGroup
    - description: Use oh:iconName (openHAB icon), f7:iconName (Framework7 icon),
        material:iconName (Material icon) or iconify:iconSet:iconName; comma
        separated list
      label: Persons Icon List
      name: personIconArray
      required: false
      type: TEXT
      groupName: customGroup
    - description: Absence person item - to display State; comma separated list
      name: personItemList
      required: false
      type: TEXT
      groupName: customGroup
    - description: "Absence person - display time since absent or display the absence
        text (default: false)"
      label: Absence Time Person - Show on Card
      name: personShowAbsentTime
      required: false
      type: BOOLEAN
      groupName: customGroup
    - description: Absence person - to display time since absence; comma separated list
      name: personItemLastUpdateList
      required: false
      type: TEXT
      groupName: customGroup
  parameterGroups:
    - name: cardGroup
      label: Card Settings
      description: All settings to customize the card layout
    - name: iconGroup
      label: Icon Settings
      description: All settings to customize the icon
    - name: headerGroup
      label: Header Settings
      description: All settings to customize the header area
    - name: footerGroup
      label: Footer Settings
      description: All settings to customize the content area
    - name: contentGroup
      label: Content Settings
      description: All settings to customize the content area
    - name: customGroup
      label: Custom Settings
      description: All settings to customize the custom objects
timestamp: Dec 30, 2025, 6:35:57 PM
component: f7-card
config:
  style:
    --f7-card-bg-color: "=props.cardBgColor ? props.cardBgColor : (themeOptions.dark
      === 'dark' ? '#232324' : 'white')"
    --f7-card-border-radius: "=props.cardBorderRadius ? (props.cardBorderRadius) + 'px' : '20px'"
    --f7-card-margin-horizontal: 4px
    --f7-card-margin-vertical: 4px
    --f7-card-outline-border-color: "=props.cardBorderColor ? props.cardBorderColor
      : (themeOptions.dark === 'dark' ? '#c95826' : '#c95826')"
    background-color: var(--f7-card-bg-color)
    background-image: "=props.cardBgImageShow== 'false' ? '' :
      'linear-gradient(rgba(255,255,255,0.6), rgba(255,255,255,0.6)),
      url(/static/' + props.cardBgImage + ')'"
    border: "=props.cardBorderSize ? (props.cardBorderSize)+'px solid
      var(--f7-card-outline-border-color)' : '6px solid
      var(--f7-card-outline-border-color)'"
    color: "=props.contentTextColor ? props.contentTextColor : (themeOptions.dark
      === 'dark' ? 'white' : 'black')"
    font-size: "=props.contentTextSize ? (props.contentTextSize)+'px' : '18px'"
slots:
  default:
    - component: f7-card-header
      config:
        style:
          --f7-card-header-border-color: transparent
          --f7-card-header-font-size: "=props.headerTextSize ? (props.headerTextSize) + 'px' : '25px'"
          --f7-card-header-font-weight: 800
          --f7-card-header-min-height: "=props.headerShow == 'true' ? '35px' : '0px'"
          --f7-card-header-text-color: "=props.headerTextColor ? props.headerTextColor :
            (themeOptions.dark === 'dark' ? 'white' : 'black')"
          justify-content: flex-start
          margin-left: "=(Number.parseInt(props.cardBorderRadius) > 90) ? '30px' :
            (Number.parseInt(props.cardBorderRadius) > 50) ? '10px' : '0px'"
      slots:
        default:
          - component: oh-icon
            config:
              icon: "=props.iconName ? props.iconName : 'f7:house'"
              style:
                color: "=props.iconColor ? props.iconColor : (themeOptions.dark === 'dark' ?
                  'white' : 'black')"
                padding-right: 10px
              visible: "=props.iconShow ? props.iconShow : props.iconShow"
              width: "=props.iconSize ? (props.iconSize) + 'px' : '30px'"
          - component: Label
            config:
              text: "=props.headerText ? props.headerText : 'Header'"
              visible: "=props.headerShow ? props.headerShow : props.headerShow"
    - component: f7-card-content
      config:
        style:
          align-items: center
          display: flex
          height: "=props.cardHeight ? (props.cardHeight) + 'px' : ''"
          justify-content: center
          margin-left: "=(Number.parseInt(props.cardBorderRadius) > 90) ? '30px' :
            (Number.parseInt(props.cardBorderRadius) > 50) ? '10px' : '0px'"
          margin-top: -10px
      slots:
        default:
          - component: f7-block
            config:
              style:
                margin-top: -10px
                text-align: center
                width: 100%
            slots:
              default:
                - component: f7-row
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: listitem
                          fragment: true
                          in: =props.personLabelArray.split(",")
                        slots:
                          default:
                            - component: f7-col
                              slots:
                                default:
                                  - component: Label
                                    config:
                                      text: =loop.listitem
                                      visible: "=props.personLabelShow ? props.personLabelShow :
                                        props.personLabelShow"
                - component: f7-row
                  config:
                    style:
                      margin-top: 10px
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: listitem
                          fragment: true
                          in: =props.personIconArray.split(",")
                        slots:
                          default:
                            - component: f7-col
                              slots:
                                default:
                                  - component: oh-icon
                                    config:
                                      action: analyzer
                                      actionAnalyzerChartType: day
                                      actionAnalyzerCoordSystem: time
                                      actionAnalyzerItems: 
                                        - '=props.personItemList.split(",")[loop.listitem_idx]'
                                      height: "=props.contentIconSize ? (props.contentIconSize) + 'px' : '30px'"
                                      icon: =loop.listitem
                                      style:
                                        color: "=(@@loop.listitem == 'ON' ? (props.presentIconColor ?
                                          props.presentIconColor : 'green') :
                                          (props.absenceIconColor ?
                                          props.absenceIconColor : 'red')) :
                                          props.contentIconColor ?
                                          props.contentIconColor :
                                          (themeOptions.dark === 'dark' ?
                                          'white' : 'black')"
                                        margin-top: 5px
                - component: f7-row
                  config:
                    class:
                      - text-align-center
                  slots:
                    default:
                      - component: oh-repeater
                        config:
                          for: listitem
                          fragment: true
                          in: =props.personItemList.split(",")
                        slots:
                          default:
                            - component: f7-col
                              slots:
                                default:
                                  - component: Label
                                    config:
                                      style:
                                        margin-top: 5px
                                      text: "=@@loop.listitem == 'ON' ? (props.presentTextValue ?
                                        props.presentTextValue : 'present')
                                        :(props.personShowAbsentTime
                                        ?  dayjs(items[props.personItemLastUpda\
                                        teList.split(',')[loop.listitem_idx]].s\
                                        tate).fromNow() :
                                        (props.absenceTextValue ?
                                        props.absenceTextValue : 'absent'))"
    - component: f7-card-footer
      config:
        style:
          --f7-card-footer-border-color: transparent
          --f7-card-footer-font-size: "=props.footerTextSize ? (props.footerTextSize) + 'px' : '16px'"
          --f7-card-footer-font-weight: 500
          --f7-card-footer-min-height: "=props.footerShow == 'true' ? '20px' : '0px'"
          --f7-card-footer-text-color: "=props.footerTextColor ? props.footerTextColor :
            (themeOptions.dark === 'dark' ? 'white' : 'black')"
          margin-left: "=(Number.parseInt(props.cardBorderRadius) > 90) ? '30px' :
            (Number.parseInt(props.cardBorderRadius) > 50) ? '10px' : '0px'"
      slots:
        default:
          - component: Label
            config:
              text: "=props.footerText ? props.footerText : 'Footer'"
              visible: "=props.footerShow ? props.footerShow : props.footerShow"
'''