Startseite » Pokémon Widget für OpenHAB

Pokémon Widget für OpenHAB

Eines der Widgets, welches ich in meinem Pokémon-Floorplan verwende und extra dafür hergestellt habe. Folgende Funktionen sind damit beabsichtigt:

  • Animation des Pokemons
  • Auswahl des Bildes anhand der Nummer
  • Anzeige von aktuellen Mess- oder Prognosewerten als Lebensleiste
    • Definition eines totalen Maximums oder aktuellen Maximums anhand von absoluten Werten oder von Items (z.B. max. UV-Wert von 10, Tagesmaximum von 6 und dabei aktueller Wert von 3.5)
    • Verarbeitung von Einheiten
  • Anzeige auch von bis zu zwei künftigen Werten (z.B. Prognose von morgen und übermorgen)
  • Evolution der Pokemon anhand von Schwellwerten (z.B. Bisasam bei Pollenwert <= 1, Bisaknosp bis <=2 und darüber Bisafloor)
  • Öffnen von Details (Gruppenseite) bein Anklicken
  • Speziell: Messrichtungen mit individuellem Symbol (z.B. Windrichtung)

Vorbereitungen

Im Vorfeld musst du für dein OpenHAB die Pokemon als .png-Files bereitstellen. Dabei muss das File jeweils beide Bewegungsvariaten beinhalten, wie es im Archiv Pokémon Essentials V 17.2 2017 10 15 der Fall ist. Am einfachsten kannst du die Pokemon aus diesem Set einfach in deinem OpenHAB-config-Ordner im Unterordner html (ggf. /etc/openhab/html) hochladen, welcher dann unter http://[dein-openhab]/static verfügbar ist

Widget

Den Code fürs Widget kannst du in OpenHAB direkt in den Entwicklereinstellungen als neues Widget anlegen und konfigurieren.

1. Kopfbereich (uid, props, parameterGroups)

  • uid: ha_pokemon → das ist einfach die eindeutige Kennung des Widgets.
  • props → hier werden Einstellungen definiert, die man später im OpenHAB UI setzen kann (z. B. welches Pokémon angezeigt wird, welche Items für „Health“ genutzt werden usw.).
    • Beispiel: pokemon: 001 bedeutet, dass standardmäßig Pokémon Nr. 001 (Bisasam) angezeigt wird.
    • threshold2, threshold3 sind Grenzwerte: wenn die Gesundheit hoch genug ist, entwickelt sich das Pokémon in Stufe 2 oder 3.
    • health1, health2, health3 sind die Items aus OpenHAB, die den Gesundheitswert für heute, morgen oder den 3. Tag liefern.
    • max bzw. max_item geben an, wie viele maximale HP es insgesamt gibt.
    • Unter actions kann man einstellen, ob das Pokémon beim Anklicken etwas auslöst (z. B. Schalter umlegen).
    • windcircle ist eine Zusatzfunktion: hier wird ein Kreis mit Pfeil angezeigt, der z. B. eine Windrichtung darstellt.
  • parameterGroups → ordnet diese Einstellungen in Gruppen (Werte, Evolutionsstufen, Aktionen, Windkreis), damit es im Editor übersichtlicher bleibt.

2. config: stylesheet (Design und Animation)

Hier steckt das Aussehen drin, mit CSS definiert:

  • Farben & Schatten: --dark-color: #444 ist ein dunkler Grundton.
  • Animationen (@keyframes):
    • showDay10 und showDay5 steuern, wie Werte für Tag 1, 2, 3 nacheinander eingeblendet werden.
  • Health-Bar:
    • health-bar-bg → der Hintergrundbalken.
    • health-bar-fill → der grüne Füllbalken, der je nach Wert kürzer oder länger ist.
  • Text:
    • .value zeigt den Zahlenwert (z. B. 80/100).
    • .name zeigt den Pokémon-Namen.
  • Sprite-Anzeige:
    • #sprite-viewport ist der Rahmen, in dem das Pokémon angezeigt wird.
    • .sprite ist das eigentliche Pokémon-Bild, das zwischen zwei Frames „wackelt“, sodass es wie animiert wirkt.

3. Inhalt (slots: default)

Das ist die eigentliche Struktur des Widgets:

  • Wind-Kreis: ein runder Rahmen mit Pfeil (oh-icon), der sich nach dem direction-Wert dreht. Damit kann man z. B. die Windrichtung grafisch darstellen.
  • Pokémon-Container (.container):
    • Enthält die Health-Bar mit dynamischen Balken.
    • oh-repeater → sorgt dafür, dass mehrere Tage (1–3) durchlaufen und angezeigt werden können. Jeder Tag hat:
      • einen Balken (Füllung, passend zum Wert),
      • einen Wert (z. B. „85/100 kWh“),
      • und den Namen (z. B. „Pikachu +1d“ für morgen).
    • Die Animation (showDay10, showDay5) blendet die Tage automatisch abwechselnd ein, wenn mehrere Forecast-Tage aktiviert sind.
  • Pokémon-Bild (oh-button mit sprite):
    • Hier wird entschieden, welches Pokémon angezeigt wird:
      • Wenn der Wert über threshold3 liegt → Pokémon 3 (z. B. Glurak).
      • Sonst, wenn über threshold2 → Pokémon 2 (z. B. Glutexo).
      • Sonst bleibt es bei Pokémon 1 (z. B. Glumanda).
    • Das Ganze steckt in einem Button → dadurch kann man beim Antippen Aktionen auslösen (Schalter toggeln oder Gruppen-Popup öffnen).
Widget-Code
uid: ha_pokemon
tags: []
props:
  parameters:
    - default: "/static/floorplan/icons/Pokemon/Icons/icon"
      label: PNG base path
      name: base_path
      required: true
      type: TEXT
    - default: "001"
      label: Pokémon No.
      name: pokemon
      required: true
      type: TEXT
      groupName: evolutions
    - label: Threshold Evolution 2
      name: threshold2
      required: false
      type: DECIMAL
      groupName: evolutions
      advanced: true
    - default: "002"
      label: Evolution 2 No.
      name: pokemon2
      required: false
      type: TEXT
      groupName: evolutions
      advanced: true
    - label: Threshold Evolution 3
      name: threshold3
      required: false
      type: DECIMAL
      groupName: evolutions
      advanced: true
    - default: "003"
      label: Evolution 3 No.
      name: pokemon3
      required: false
      type: TEXT
      groupName: evolutions
      advanced: true
    - context: item
      label: Health Now
      name: health1
      required: true
      type: TEXT
      groupName: values
    - context: item
      label: Health Today
      name: between1
      required: false
      type: TEXT
      groupName: values
      advanced: true
    - default: "1"
      label: Forecast days (1-3)
      name: forecastDays
      required: true
      type: INTEGER
      groupName: values
      advanced: true
    - context: item
      label: Health Tomorrow
      name: health2
      required: false
      type: TEXT
      groupName: values
      advanced: true
    - context: item
      label: Health Day 3
      name: health3
      required: false
      type: TEXT
      groupName: values
      advanced: true
    - default: "100"
      label: Maximum HP
      name: max
      required: false
      type: DECIMAL
      groupName: values
    - context: item
      label: Maximum HP Item
      name: max_item
      required: false
      type: TEXT
      groupName: values
      advanced: true
    - default: "#997700"
      label: Forecast color
      name: fc_color
      required: false
      type: TEXT
      groupName: values
      advanced: true
    - label: Name
      name: name
      required: false
      type: TEXT
    - context: item
      label: Switch Action
      name: switchItem
      required: false
      type: TEXT
      groupName: actions
    - context: item
      label: Actiongroup
      name: groupName
      required: false
      type: TEXT
      groupName: actions
    - context: item
      description: Direction in deg
      label: Direction
      name: direction
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
    - default: "0"
      description: Offset in deg
      label: Direction offset
      name: direction_offset
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
    - default: "180"
      label: Rotate arrow deg
      name: rotateArrow
      required: false
      type: INTEGER
      groupName: windcircle
      advanced: true
    - default: if:mdi:arrow-compass
      label: Arrow image
      name: arrowImage
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
    - default: red
      label: Arrow color
      name: arrowColor
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
    - default: 44px
      label: Arrow height
      name: arrowHeight
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
    - default: 4px
      label: Circle width
      name: circleWidth
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
    - default: "#34e5eb"
      label: Circle color
      name: circleColor
      required: false
      type: TEXT
      groupName: windcircle
      advanced: true
  parameterGroups:
    - name: values
      label: Definitions of Health Values
      description: what items shall define the health? Forecasts for more days possible
    - name: evolutions
      label: Evolutions and thresholds
      description: what Pokemon shall be shown? Shall it evolve or change on thresholds?
    - name: actions
      label: Actions
      description: what actions shall occur on tapping or holding the pokemon?
    - name: windcircle
      label: Wind Circle
      description: style the wind direction circle
timestamp: Jul 8, 2025, 8:57:00 PM
component: f7-card
config:
  stylesheet: |
    :root {
      --dark-color: #444; /* einheitlicher dunkler Farbton */
    }

    @keyframes showDay10 {
      0%   { opacity: 1; }
      50%  { opacity: 0; }
      100% { opacity: 0; }
    }
    @keyframes showDay5 {
      0%   { opacity: 1; }
      25%  { opacity: 0; }
      100% { opacity: 0; }
    }

    .health-bar-bg {
      position: absolute;
      top: -6px;
      left: 0;
      width: 64px;
      height: 8px;
      background: var(--dark-color);
      border-radius: 4px;
    }
    .health-bar-fill {
      position: absolute;
      top: -5px;
      left: 1px;
      max-width: 62px;
      height: 6px;
      background: #4caf50;
      border-radius: 3px;
    }
    .value {
      position: absolute;
      top: 0px;
      right: 0;
      white-space: nowrap;
    }
    .name {
      position: absolute;
      top: -22px;
      left: 0;
      white-space: nowrap;
    }
    .container {
      position: relative;
      width: 64px;
      height: 64px;
      margin: 10px auto auto auto;
      font-weight: 900;
      font-size: smaller;
      font-family: monospace;
      text-shadow:
        -1px -1px 0 var(--dark-color),
         1px -1px 0 var(--dark-color),
        -1px  1px 0 var(--dark-color),
         1px  1px 0 var(--dark-color);
    }
    #sprite-viewport {
      width: 64px;
      height: 64px;
      overflow: hidden;
      position: relative;
    }
    .sprite {
      position: absolute;
      top: 0px;
      left: 0;
      width: 128px;
      height: 64px;
      background-position: left center;
      background-size: contain;
      animation: toggle-frame 1s infinite step-end;
    }
    @keyframes toggle-frame {
      0%   { left: 0; }
      50%  { left: -64px; }
      100% { left: 0; }
    }
slots:
  default:
    - component: f7-card-content
      config: {}
      slots:
        default:
          - component: div
            config:
              style:
                aspect-ratio: 1/1
                border-color: =props.circleColor
                border-radius: 50%
                border-style: solid
                border-width: =props.circleWidth
                margin: 14px auto
                position: absolute
                width: 60%
              visible: |
                =props.direction ? 'true' : 'false'
            slots:
              default:
                - component: div
                  config:
                    style:
                      height: 100%
                      transform: ="rotate(" + (Number.parseFloat(@props.direction) +
                        Number.parseFloat(props.direction_offset)) + "deg)"
                  slots:
                    default:
                      - component: oh-icon
                        config:
                          height: =props.arrowHeight
                          icon: =props.arrowImage
                          style:
                            color: =props.arrowColor
                            transform: ="translate(0, -50%) rotate(" + Number.parseInt(props.rotateArrow)
                              +"deg)"
                            width: 100%
          - component: div
            config:
              class:
                - container
            slots:
              default:
                - component: div
                  config:
                    class:
                      - health-bar-bg
                - component: oh-repeater
                  config:
                    for: days
                    rangeStart: 1
                    rangeStop: |
                      =props.forecastDays ? Number(props.forecastDays) : 1
                    sourceType: range
                  slots:
                    default:
                      - component: div
                        config:
                          class:
                            - health-bar-fill
                            - ='day' + loop.days
                          style:
                            animation: "=(props.forecastDays > 1 && loop.days == 1) ? ('showDay10 20s
                              infinite steps(1)') : ''"
                            animation-delay: 0s
                            background-color: =props.fc_color
                            opacity: "=(props.forecastDays > 1 && loop.days > 1) ? '0' : '1'"
                            width: >
                              =(props['between' + loop.days] ? Math.max(
                                items[props['between' + loop.days]].displayState ?
                                  (Number(items[props['between' + loop.days]].displayState.split(" ")[0].replace(',','.')) / (props.max_item ? Number(items[props.between].displayState.split(" ")[0].replace(',','.')) : props.max) * 100)
                                : (items[props.between].numericState / (props.max_item ? items[props.max_item].numericState : props.max) * 100)
                                ,0
                              ) : "0") + "%"
                      - component: div
                        config:
                          class:
                            - health-bar-fill
                            - ='day' + loop.days
                          style:
                            animation: "=(props.forecastDays > 1) ? ('showDay' + (loop.days == 1 ? '10' :
                              '5') + ' ' + ((props.forecastDays+1)*5) + 's
                              infinite steps(1)') : ''"
                            animation-delay: "=(loop.days == 1) ? '0s' : (loop.days*5) + 's'"
                            background-color: "=loop.days > 1 ? props.fc_color : ''"
                            opacity: "=props.forecastDays > 1 ? '0' : '1'"
                            width: >
                              =(props['health' + loop.days] ? Math.max(
                                items[props['health' + loop.days]].displayState ?
                                  (Number(items[props['health' + loop.days]].displayState.split(" ")[0].replace(',','.')) / (props.max_item ? Number(items[props['health' + loop.days]].displayState.split(" ")[0].replace(',','.')) : props.max) * 100)
                                : (items[props['health' + loop.days]].numericState / (props.max_item ? items[props.max_item].numericState : props.max) * 100)
                                ,0
                              ) : "0") + "%"
                      - component: Label
                        config:
                          class:
                            - value
                            - ='day' + loop.days
                          style:
                            animation: "=(props.forecastDays > 1) ? ('showDay' + (loop.days == 1 ? '10' :
                              '5') + ' ' + ((props.forecastDays+1)*5) + 's
                              infinite steps(1)') : ''"
                            animation-delay: "=(loop.days == 1) ? '0s' : (loop.days*5) + 's'"
                            opacity: "=props.forecastDays > 1 ? '0' : '1'"
                          text: >
                            = props['health' + loop.days] ? (
                                (items[props['health' + loop.days]].displayState && items[props['health' + loop.days]].displayState.split(" ").length == 2 ?
                                  items[props['health' + loop.days]].displayState.split(" ")[0].replace(',','.')
                                  : items[props['health' + loop.days]].numericState ?
                                    items[props['health' + loop.days]].numericState
                                    : 0
                                )
                                + "/" + (props.max_item ? items[props.max_item].numericState : Math.max(props.max,(items[props['health' + loop.days]].numericState ? items[props['health' + loop.days]].numericState : 0)))
                                + " "
                                +(items[props['health' + loop.days]].displayState && items[props['health' + loop.days]].displayState.split(" ").length == 2 ? items[props['health' + loop.days]].displayState.split(" ")[1] : (
                                  (items[props['health' + loop.days]].unit && items[props['health' + loop.days]].unit != "one" ? items[props['health' + loop.days]].unit : "")
                                ))
                              ) : ""
                      - component: Label
                        config:
                          class:
                            - name
                            - ='day' + loop.days
                          style:
                            animation: "=(props.forecastDays > 1) ? ('showDay' + (loop.days == 1 ? '10' :
                              '5') + ' ' + ((props.forecastDays+1)*5) + 's
                              infinite steps(1)') : ''"
                            animation-delay: "=(loop.days == 1) ? '0s' : (loop.days*5) + 's'"
                            opacity: "=props.forecastDays > 1 ? '0' : '1'"
                          text: >
                            = (props.name ? props.name : "") + (loop.days > 1 ?
                            ' +' + (loop.days-1) + 'd' : '')
                - component: oh-button
                  config:
                    action: "=(props.switchItem) ? 'toggle' : 'group'"
                    actionCommand: ON
                    actionCommandAlt: OFF
                    actionGroupPopupItem: "=props.groupName ? props.groupName : props.health1"
                    actionItem: =props.switchItem
                    id: sprite-viewport
                    taphold_action: group
                    taphold_actionGroupPopupItem: "=props.groupName ? props.groupName : props.health1"
                  slots:
                    default:
                      - component: div
                        config:
                          class:
                            - sprite
                            - ='pokemon' + (props.health)
                          style:
                            background-image: >
                              =(props.threshold3 &&
                              items[props.health1]?.numericState >=
                              props.threshold3) ? (
                                "url('" + props.base_path + props.pokemon3 + ".png')"
                               ) : (
                                (props.threshold2 && items[props.health1]?.numericState >= props.threshold2) ?
                                  "url('" + props.base_path + props.pokemon2 + ".png')"
                                  : "url('" + props.base_path + props.pokemon + ".png')"
                               )
                            background-repeat: no-repeat

1 Kommentar zu „Pokémon Widget für OpenHAB“

  1. Pingback: Interaktiver Floorplan in OpenHAB mit Effekten

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.

Translate »