Widget LG ThinQ Washing Machine — animated mobile card [5.0.0.0;6.0.0.0)

A self-contained, phone-optimised widget for LG washing machines exposed through the LG ThinQ binding (lgthinq). Pure SVG, dark navy theme; everything animates while a cycle runs — a rotating progress ring, a live drum with three paddles, water droplets, and a 5-stage progress bar (Weigh → Wash → Rinse → Spin → Done). A single f7-card, no external images or fonts. Built for a phone in a single column (portrait viewBox 720×1060, scales to container width).

What it shows

  • Header status pill (top-right): ON / STANDBY / OFF, current phase, time remaining
  • Central ring: rotates while running, amber on pause, full green when finished; large remaining-time readout (H:MM) in the middle
  • Drawn washer with a spinning drum (3 paddles), display LED and water droplets — animated only while a cycle is active
  • Estimated finish time (“done at ~HH:MM”, via dayjs)
  • Stage bar: Weigh → Wash → Rinse → Spin → Done; completed steps turn green, the active one pulses
  • Six info tiles: Program/Course, Temperature, Spin speed, Rinse, Door lock, Remote start

Raw ThinQ enum values (e.g. TURBO59, TEMP_40, SPIN_1200, DOOR_LOCK_ON) are mapped to friendly labels inside the widget.

Requirements

  • openHAB 5.x (Main UI), tested on 5.2. Likely works on 4.x but untested — if you confirm it, the title range can be widened to [4.0.0.0;....
  • LG ThinQ binding with a washer-201 (or compatible) thing online.
  • Channels linked to items (all props optional — leave one blank to hide that piece):
Prop Channel (typical) Item type
itemPower dashboard#power Switch
itemStandby dashboard#stand-by Switch
itemState dashboard#state String
itemProcess dashboard#process-state String
itemCourse dashboard#course String
itemTemp dashboard#temperature-level String
itemSpin dashboard#spin String
itemRinse dashboard#rinse String
itemRemaining dashboard#remain-time String (H:MM)
itemDoor dashboard#door-lock String (DOOR_LOCK_ON/OFF or 1/0)
itemRemoteStart dashboard#rs-flag Switch

state is the coarse washer state; process-state is the detailed phase. The widget prefers process-state for the ring / stage bar / drum and falls back to state when process-state isn’t reporting a known phase — link both for the best result.

Adapting value maps to your model: the friendly-text maps cover common LG course/temperature/spin/rinse enums. If your model reports a course that isn’t mapped, the tile shows ; add your value to the object in the PROGRAM text element (same pattern for temperature/spin/rinse). Find your model’s exact values via the item’s State Description in the Developer Sidebar, or the event log while you change settings on the machine.

Screenshots

Changelog

Version 0.1

  • initial release: animated ring + drawn washer with spinning drum, 5-stage bar, six info tiles, friendly value maps, process-statestate fallback, door lock accepts both DOOR_LOCK_ON/OFF and 1/0

Resources

uid: washer_lg_card_mobile
tags:
- washer
- mobile
- LG
props:
  parameters:
  - context: item
    description: Switch item for washer power on/off
    label: Power (Switch)
    name: itemPower
    required: false
    type: TEXT
  - context: item
    description: Switch item for standby mode
    label: Standby mode (Switch)
    name: itemStandby
    required: false
    type: TEXT
  - context: item
    description: 'String item: overall washer state (RUNNING, END, PAUSE, ...)'
    label: Washer State (String)
    name: itemState
    required: false
    type: TEXT
  - context: item
    description: 'String item: detailed cycle phase (DETECTING, RINSING, SPINNING, ...). Drives the ring, stage bar and drum.'
    label: Process State / phase (String)
    name: itemProcess
    required: false
    type: TEXT
  - context: item
    description: 'String item: selected program / course'
    label: Course / program (String)
    name: itemCourse
    required: false
    type: TEXT
  - context: item
    description: 'String item: temperature level (TEMP_40, TEMP_COLD, ...)'
    label: Temperature level (String)
    name: itemTemp
    required: false
    type: TEXT
  - context: item
    description: 'String item: spin speed (SPIN_1200, NO_SPIN, ...)'
    label: Spin (String)
    name: itemSpin
    required: false
    type: TEXT
  - context: item
    description: 'String item: rinse option (RINSE_NORMAL, RINSE_PLUS, ...)'
    label: Rinse (String)
    name: itemRinse
    required: false
    type: TEXT
  - context: item
    description: 'String item: remaining time, formatted H:MM'
    label: Remaining time (String)
    name: itemRemaining
    required: false
    type: TEXT
  - context: item
    description: 'String item: door lock (accepts DOOR_LOCK_ON/OFF or 1/0)'
    label: Door lock (String)
    name: itemDoor
    required: false
    type: TEXT
  - context: item
    description: 'Switch item: remote start enabled'
    label: Remote start (Switch)
    name: itemRemoteStart
    required: false
    type: TEXT
component: f7-card
config:
  style:
    background: '#0b111a'
    border-radius: 28px
    box-shadow: 0 6px 24px rgba(0,0,0,0.45)
    margin: 8px
    overflow: hidden
    padding: '0'
    position: relative
  stylesheet: |-
    .wm-pulse { animation: wm-pulse 2s ease-in-out infinite; }
    @keyframes wm-pulse { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
    .wm-ring { transform-box: fill-box; transform-origin: center; animation: wm-spin 2.4s linear infinite; }
    .wm-drum { transform-box: fill-box; transform-origin: center; animation: wm-spin 1.7s linear infinite; }
    @keyframes wm-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
slots:
  default:
  - component: svg
    config:
      style:
        display: block
      viewBox: 0 0 720 1060
      width: 100%
      xmlns: http://www.w3.org/2000/svg
    slots:
      default:
      - component: rect
        config:
          fill: '#0b111a'
          height: '1060'
          width: '720'
          x: '0'
          y: '0'
      - component: circle
        config:
          cx: '360'
          cy: '400'
          fill: none
          r: '225'
          stroke: '#16222f'
          stroke-width: '1.5'
      - component: circle
        config:
          cx: '360'
          cy: '400'
          fill: none
          r: '250'
          stroke: '#14202c'
          stroke-dasharray: 4 10
          stroke-width: '1.5'
      - component: rect
        config:
          fill: rgba(255,255,255,0.04)
          height: '44'
          rx: '22'
          stroke: rgba(255,255,255,0.1)
          width: '290'
          x: '40'
          y: '28'
      - component: circle
        config:
          class: wm-pulse
          cx: '64'
          cy: '50'
          fill: "=(items[props.itemPower].state === 'ON') ? '#37d67a' : ((items[props.itemStandby].state === 'ON') ? '#ffd54f' : '#8a97a3')"
          r: '5'
      - component: text
        config:
          content: "=(items[props.itemPower].state === 'ON') ? 'WASHER ON' : ((items[props.itemStandby].state === 'ON') ? 'STANDBY' : 'WASHER OFF')"
          fill: "=(items[props.itemPower].state === 'ON') ? '#37d67a' : ((items[props.itemStandby].state === 'ON') ? '#ffd54f' : '#9fb0bd')"
          font-size: '18'
          font-weight: '600'
          letter-spacing: '1'
          x: '80'
          y: '57'
      - component: text
        config:
          content: =dayjs().format('DD.MM.YYYY')
          fill: '#dbe3ea'
          font-size: '18'
          font-weight: '600'
          text-anchor: end
          x: '680'
          y: '57'
      - component: text
        config:
          content: LG WASHER
          fill: '#ffffff'
          font-size: '34'
          font-weight: '800'
          letter-spacing: '2'
          x: '40'
          y: '116'
      - component: text
        config:
          content: WASHING MACHINE · LG THINQ
          fill: '#9fb0bd'
          font-size: '17'
          letter-spacing: '3'
          x: '40'
          y: '144'
      - component: rect
        config:
          fill: rgba(255,255,255,0.03)
          height: '64'
          rx: '18'
          stroke: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? 'rgba(53,194,245,0.5)' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'PAUSE' || [(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'RINSEHOLD') ? 'rgba(255,213,79,0.5)' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? 'rgba(55,214,122,0.5)' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'ERROR') ? 'rgba(255,112,67,0.5)' : 'rgba(159,176,189,0.3)')))"
          width: '268'
          x: '412'
          y: '84'
      - component: text
        config:
          content: "=({'DETECTING':'⚖️','WASHING':'🌀','RUNNING':'🌀','RINSING':'💧','SPINNING':'🌀','DRYING':'🔥','STEAMSOFTENING':'♨️','WASH_REFRESHING':'✨','COOLDOWN':'❄️','PAUSE':'⏸','RINSEHOLD':'⏸','END':'✅','ERROR':'⚠️','RESERVED':'🕐','INITIAL':'🟢'})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state] || '🌙'"
          font-size: '20'
          x: '434'
          y: '124'
      - component: text
        config:
          content: "=({'DETECTING':'WEIGHING','WASHING':'WASHING','RUNNING':'WASHING','RINSING':'RINSING','SPINNING':'SPINNING','DRYING':'DRYING','STEAMSOFTENING':'STEAM','WASH_REFRESHING':'REFRESHING','COOLDOWN':'COOLDOWN','PAUSE':'PAUSED','RINSEHOLD':'RINSE HOLD','END':'CYCLE DONE','ERROR':'ERROR','RESERVED':'SCHEDULED','INITIAL':'READY'})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state] || 'OFF'"
          fill: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? '#35c2f5' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'PAUSE' || [(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'RINSEHOLD') ? '#ffd54f' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '#37d67a' : '#8a97a3'))"
          font-size: '19'
          font-weight: '800'
          x: '470'
          y: '114'
      - component: text
        config:
          content: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0 && items[props.itemRemaining].state && items[props.itemRemaining].state !== 'NULL') ? ('⏱ ' + items[props.itemRemaining].state + ' left') : 'no active cycle'"
          fill: '#9fb0bd'
          font-size: '14'
          x: '470'
          y: '138'
      - component: circle
        config:
          cx: '360'
          cy: '400'
          fill: none
          r: '190'
          stroke: '#16222f'
          stroke-width: '18'
      - component: g
        config:
          transform: rotate(-90 360 400)
        slots:
          default:
          - component: circle
            config:
              class: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? 'wm-ring' : ''"
              cx: '360'
              cy: '400'
              fill: none
              opacity: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? '1' : '0.25'"
              r: '190'
              stroke: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? '#35c2f5' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'PAUSE' || [(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'RINSEHOLD') ? '#ffd54f' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '#37d67a' : '#2a3644'))"
              stroke-dasharray: "=([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '1194 1' : '420 774'"
              stroke-linecap: round
              stroke-width: '18'
      - component: text
        config:
          content: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? 'TIME LEFT' : 'STATUS'"
          fill: '#9fb0bd'
          font-size: '16'
          letter-spacing: '2'
          text-anchor: middle
          x: '360'
          y: '272'
      - component: text
        config:
          content: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? ((items[props.itemRemaining].state && items[props.itemRemaining].state !== 'NULL' && items[props.itemRemaining].state !== 'UNDEF') ? items[props.itemRemaining].state : '--:--') : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '✔' : '—')"
          fill: '#ffffff'
          font-size: '48'
          font-weight: '800'
          text-anchor: middle
          x: '360'
          y: '320'
      - component: ellipse
        config:
          cx: '360'
          cy: '564'
          fill: '#000000'
          opacity: '0.45'
          rx: '80'
          ry: '11'
      - component: rect
        config:
          fill: '#30363e'
          height: '220'
          rx: '14'
          stroke: '#454c55'
          stroke-width: '2.5'
          width: '164'
          x: '278'
          y: '336'
      - component: line
        config:
          stroke: '#454c55'
          stroke-width: '2.5'
          x1: '280'
          x2: '440'
          y1: '378'
          y2: '378'
      - component: rect
        config:
          fill: '#3a414a'
          height: '18'
          rx: '3'
          width: '32'
          x: '288'
          y: '350'
      - component: circle
        config:
          cx: '350'
          cy: '359'
          fill: '#4a525c'
          r: '9'
          stroke: '#59626d'
          stroke-width: '1.5'
      - component: rect
        config:
          fill: '#35c2f5'
          height: '15'
          opacity: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? '0.9' : '0.25'"
          rx: '3'
          width: '38'
          x: '390'
          y: '352'
      - component: circle
        config:
          cx: '360'
          cy: '478'
          fill: '#1c2127'
          r: '58'
          stroke: '#4a525c'
          stroke-width: '5'
      - component: circle
        config:
          cx: '360'
          cy: '478'
          fill: none
          r: '46'
          stroke: '#343b44'
          stroke-width: '2.5'
      - component: circle
        config:
          cx: '360'
          cy: '478'
          fill: '#0d1520'
          r: '38'
      - component: g
        config:
          transform: translate(360,478)
        slots:
          default:
          - component: g
            config:
              class: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? 'wm-drum' : ''"
            slots:
              default:
              - component: circle
                config:
                  cx: '0'
                  cy: '0'
                  fill: none
                  opacity: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? '0.5' : '0.15'"
                  r: '27'
                  stroke: '#2f9fe0'
                  stroke-dasharray: 54 116
                  stroke-linecap: round
                  stroke-width: '4'
              - component: rect
                config:
                  fill: '#3f4750'
                  height: '22'
                  rx: '4'
                  width: '8'
                  x: '-4'
                  y: '-32'
              - component: rect
                config:
                  fill: '#3f4750'
                  height: '22'
                  rx: '4'
                  transform: rotate(120)
                  width: '8'
                  x: '-4'
                  y: '-32'
              - component: rect
                config:
                  fill: '#3f4750'
                  height: '22'
                  rx: '4'
                  transform: rotate(240)
                  width: '8'
                  x: '-4'
                  y: '-32'
              - component: circle
                config:
                  cx: '0'
                  cy: '0'
                  fill: '#333a43'
                  r: '6'
      - component: rect
        config:
          fill: '#454c55'
          height: '9'
          rx: '2'
          width: '16'
          x: '292'
          y: '556'
      - component: rect
        config:
          fill: '#454c55'
          height: '9'
          rx: '2'
          width: '16'
          x: '412'
          y: '556'
      - component: circle
        config:
          class: wm-pulse
          cx: '240'
          cy: '428'
          fill: '#2f9fe0'
          opacity: '0.35'
          r: '7'
      - component: circle
        config:
          class: wm-pulse
          cx: '255'
          cy: '468'
          fill: '#2f9fe0'
          opacity: '0.3'
          r: '5'
      - component: circle
        config:
          class: wm-pulse
          cx: '230'
          cy: '500'
          fill: '#2f9fe0'
          opacity: '0.3'
          r: '4'
      - component: circle
        config:
          class: wm-pulse
          cx: '480'
          cy: '440'
          fill: '#2f9fe0'
          opacity: '0.3'
          r: '6'
      - component: circle
        config:
          class: wm-pulse
          cx: '494'
          cy: '482'
          fill: '#2f9fe0'
          opacity: '0.25'
          r: '4'
      - component: text
        config:
          content: "=(({'DETECTING':'Weighing load','WASHING':'Washing','RUNNING':'Washing','RINSING':'Rinsing','SPINNING':'Spinning','DRYING':'Drying','STEAMSOFTENING':'Steam softening','WASH_REFRESHING':'Refreshing','COOLDOWN':'Cooling down','PAUSE':'Paused','RINSEHOLD':'Rinse hold','END':'Finished','ERROR':'Error','RESERVED':'Scheduled start','INITIAL':'Ready to start'})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state] || 'Off') + ((['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0 && items[props.itemRemaining].state && items[props.itemRemaining].state.indexOf(':') > 0) ? (' · done at ~' + dayjs().add(Number.parseInt(items[props.itemRemaining].state.split(':')[0]) || 0, 'hour').add(Number.parseInt(items[props.itemRemaining].state.split(':')[1]) || 0, 'minute').format('HH:mm')) : '')"
          fill: "=(['DETECTING','WASHING','RUNNING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN'].indexOf((['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state) >= 0) ? '#35c2f5' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '#37d67a' : '#9fb0bd')"
          font-size: '20'
          font-weight: '700'
          text-anchor: middle
          x: '360'
          y: '632'
      - component: line
        config:
          stroke: '#22303f'
          stroke-width: '4'
          x1: '110'
          x2: '610'
          y1: '680'
          y2: '680'
      - component: line
        config:
          stroke: '#37d67a'
          stroke-width: '4'
          x1: '110'
          x2: "=110 + 125 * Math.max(0, ({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state] || 0)"
          y1: '680'
          y2: '680'
      - component: circle
        config:
          class: "=([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'DETECTING') ? 'wm-pulse' : ''"
          cx: '110'
          cy: '680'
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) > 0 ? '#37d67a' : (([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'DETECTING') ? '#35c2f5' : '#1b2531')"
          r: '12'
          stroke: rgba(255,255,255,0.12)
          stroke-width: '2'
      - component: circle
        config:
          class: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 1 ? 'wm-pulse' : ''"
          cx: '235'
          cy: '680'
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) > 1 ? '#37d67a' : ((({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 1 ? '#35c2f5' : '#1b2531')"
          r: '12'
          stroke: rgba(255,255,255,0.12)
          stroke-width: '2'
      - component: circle
        config:
          class: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 2 ? 'wm-pulse' : ''"
          cx: '360'
          cy: '680'
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) > 2 ? '#37d67a' : ((({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 2 ? '#35c2f5' : '#1b2531')"
          r: '12'
          stroke: rgba(255,255,255,0.12)
          stroke-width: '2'
      - component: circle
        config:
          class: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 3 ? 'wm-pulse' : ''"
          cx: '485'
          cy: '680'
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) > 3 ? '#37d67a' : ((({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 3 ? '#35c2f5' : '#1b2531')"
          r: '12'
          stroke: rgba(255,255,255,0.12)
          stroke-width: '2'
      - component: circle
        config:
          class: "=([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? 'wm-pulse' : ''"
          cx: '610'
          cy: '680'
          fill: "=([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '#37d67a' : '#1b2531'"
          r: '12'
          stroke: rgba(255,255,255,0.12)
          stroke-width: '2'
      - component: text
        config:
          content: WEIGH
          fill: "=([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'DETECTING') ? '#e6ecf1' : '#74848f'"
          font-size: '15'
          text-anchor: middle
          x: '110'
          y: '722'
      - component: text
        config:
          content: WASH
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 1 ? '#e6ecf1' : '#74848f'"
          font-size: '15'
          text-anchor: middle
          x: '235'
          y: '722'
      - component: text
        config:
          content: RINSE
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 2 ? '#e6ecf1' : '#74848f'"
          font-size: '15'
          text-anchor: middle
          x: '360'
          y: '722'
      - component: text
        config:
          content: SPIN
          fill: "=(({'DETECTING':0,'WASHING':1,'RUNNING':1,'STEAMSOFTENING':1,'WASH_REFRESHING':1,'RINSING':2,'RINSEHOLD':2,'SPINNING':3,'DRYING':3,'COOLDOWN':3,'END':4})[(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state]) === 3 ? '#e6ecf1' : '#74848f'"
          font-size: '15'
          text-anchor: middle
          x: '485'
          y: '722'
      - component: text
        config:
          content: DONE
          fill: "=([(['DETECTING','WASHING','RINSING','SPINNING','DRYING','STEAMSOFTENING','WASH_REFRESHING','COOLDOWN','PAUSE','RINSEHOLD','END'].indexOf(items[props.itemProcess].state) >= 0) ? items[props.itemProcess].state : items[props.itemState].state][0] === 'END') ? '#e6ecf1' : '#74848f'"
          font-size: '15'
          text-anchor: middle
          x: '610'
          y: '722'
      - component: rect
        config:
          fill: rgba(255,255,255,0.035)
          height: '84'
          rx: '18'
          stroke: rgba(255,255,255,0.09)
          width: '312'
          x: '40'
          y: '752'
      - component: text
        config:
          content: 🧺
          font-size: '22'
          x: '60'
          y: '804'
      - component: text
        config:
          content: PROGRAM
          fill: '#9fb0bd'
          font-size: '17'
          font-weight: '600'
          x: '100'
          y: '788'
      - component: text
        config:
          content: =({'COTTON':'Cotton','COTTONPLUS':'Eco 40-60','MIXEDFABRIC':'Mixed fabric','EASYCARE':'Easy care','TURBO59':'Turbo Wash 59','SPEED14':'Speed 14','SMALLLOAD':'Small load','SINGLEGARMENT':'Single garment','LIGHTLYSOILEDITEMS':'Lightly soiled','DUVET':'Duvet','BLANKET':'Blanket','WOOL':'Wool','DELICATE':'Delicate','LINGERIE':'Lingerie','JEANS':'Jeans','SPORTSWEAR':'Sportswear','GYMCLOTHES':'Gym clothes','SWIMMINGWEAR':'Swimwear','KIDSWEAR':'Kids wear','BABYCARE':'Baby care','BABYSTEAMCARE':'Baby steam','ALLERGYSPASTEAM':'Anti-allergy','HYGIENE':'Hygiene','SKINCARE':'Skin care','COLORPROTECTION':'Color protection','COLDWASH':'Cold wash','NOISEMINIMIZE':'Quiet','RAINYSEASON':'Rainy season','SWEATSTAIN':'Sweat stains','JUICEANDFOODSTAINS':'Food stains','SLEEVEHEMSANDCOLLARS':'Cuffs & collars','MINIMIZEDETERGENT':'Less detergent','RINSESPIN':'Rinse + spin','SPIN':'Spin','DRAIN':'Drain','TUB_CLEAN':'Tub clean','QUICKTUBCLEAN':'Quick tub clean','NOT_SELECTED':'—'})[items[props.itemCourse].state] || '—'
          fill: '#ffffff'
          font-size: '19'
          font-weight: '800'
          x: '100'
          y: '822'
      - component: rect
        config:
          fill: rgba(255,255,255,0.035)
          height: '84'
          rx: '18'
          stroke: rgba(255,255,255,0.09)
          width: '312'
          x: '368'
          y: '752'
      - component: text
        config:
          content: 🌡
          font-size: '22'
          x: '388'
          y: '804'
      - component: text
        config:
          content: TEMPERATURE
          fill: '#9fb0bd'
          font-size: '17'
          font-weight: '600'
          x: '428'
          y: '788'
      - component: text
        config:
          content: =({'TEMP_COLD':'Cold','TEMP_20':'20 °C','TEMP_30':'30 °C','TEMP_40':'40 °C','TEMP_50':'50 °C','TEMP_60':'60 °C','TEMP_95':'95 °C','NO_TEMP':'—'})[items[props.itemTemp].state] || '—'
          fill: '#ffffff'
          font-size: '22'
          font-weight: '800'
          x: '428'
          y: '822'
      - component: rect
        config:
          fill: rgba(255,255,255,0.035)
          height: '84'
          rx: '18'
          stroke: rgba(255,255,255,0.09)
          width: '312'
          x: '40'
          y: '848'
      - component: text
        config:
          content: 🌀
          font-size: '22'
          x: '60'
          y: '900'
      - component: text
        config:
          content: SPIN
          fill: '#9fb0bd'
          font-size: '17'
          font-weight: '600'
          x: '100'
          y: '884'
      - component: text
        config:
          content: "=(items[props.itemSpin].state === 'NO_SPIN') ? 'No spin' : ((items[props.itemSpin].state === 'SPIN_Max') ? 'Max' : ((items[props.itemSpin].state && items[props.itemSpin].state.indexOf('SPIN_') === 0) ? (items[props.itemSpin].state.substring(5) + ' rpm') : '—'))"
          fill: '#ffffff'
          font-size: '22'
          font-weight: '800'
          x: '100'
          y: '918'
      - component: rect
        config:
          fill: rgba(255,255,255,0.035)
          height: '84'
          rx: '18'
          stroke: rgba(255,255,255,0.09)
          width: '312'
          x: '368'
          y: '848'
      - component: text
        config:
          content: 💧
          font-size: '22'
          x: '388'
          y: '900'
      - component: text
        config:
          content: RINSE
          fill: '#9fb0bd'
          font-size: '17'
          font-weight: '600'
          x: '428'
          y: '884'
      - component: text
        config:
          content: =({'RINSE_NORMAL':'Normal','RINSE_PLUS':'Plus','RINSE_PLUSPLUS':'Plus ++','RINSE_NORMAL_HOLD':'Normal · hold','RINSE_PLUS_HOLD':'Plus · hold','NO_RINSE':'—'})[items[props.itemRinse].state] || '—'
          fill: '#ffffff'
          font-size: '22'
          font-weight: '800'
          x: '428'
          y: '918'
      - component: rect
        config:
          fill: rgba(255,255,255,0.035)
          height: '84'
          rx: '18'
          stroke: rgba(255,255,255,0.09)
          width: '312'
          x: '40'
          y: '944'
      - component: text
        config:
          content: "=(items[props.itemDoor].state === '1' || items[props.itemDoor].state === 'DOOR_LOCK_ON') ? '🔒' : '🔓'"
          font-size: '22'
          x: '60'
          y: '996'
      - component: text
        config:
          content: DOOR
          fill: '#9fb0bd'
          font-size: '17'
          font-weight: '600'
          x: '100'
          y: '980'
      - component: text
        config:
          content: "=(items[props.itemDoor].state === '1' || items[props.itemDoor].state === 'DOOR_LOCK_ON') ? 'LOCKED' : ((items[props.itemDoor].state === '0' || items[props.itemDoor].state === 'DOOR_LOCK_OFF') ? 'UNLOCKED' : '—')"
          fill: "=(items[props.itemDoor].state === '1' || items[props.itemDoor].state === 'DOOR_LOCK_ON') ? '#35c2f5' : '#9fb0bd'"
          font-size: '22'
          font-weight: '800'
          x: '100'
          y: '1014'
      - component: rect
        config:
          fill: rgba(255,255,255,0.035)
          height: '84'
          rx: '18'
          stroke: rgba(255,255,255,0.09)
          width: '312'
          x: '368'
          y: '944'
      - component: text
        config:
          content: 📡
          font-size: '22'
          x: '388'
          y: '996'
      - component: text
        config:
          content: REMOTE START
          fill: '#9fb0bd'
          font-size: '17'
          font-weight: '600'
          x: '428'
          y: '980'
      - component: text
        config:
          content: "=(items[props.itemRemoteStart].state === 'ON') ? 'ENABLED' : 'DISABLED'"
          fill: "=(items[props.itemRemoteStart].state === 'ON') ? '#37d67a' : '#9fb0bd'"
          font-size: '22'
          font-weight: '800'
          x: '428'
          y: '1014'

Since this was formatted as a marketplace add-on, changed the category from Add-on to Add-on Marketplace. Otherwise, it won’t show up as installable in OH. I wasn’t sure what tags to apply, to I picked stable and published. If that doesn’t fit, please revise the tags.