KI-gestützte openHAB-Entwicklung mit Visual Studio Code und openAI codex

openHAB-Entwicklung kann recht mühsam sein. Gerade bei Datum-Zeit-Funktionen habe ich openHAB schon oft verflucht und die Entwicklung von Widgets fand ich auch stets sehr mühsam. Zeit, zu testen, wie das mit KI funktioniert…

Was ich zuvor schon getestet habe, war Code in chatGPT zu werfen und verbessern zu lassen. Das hat oft schon etwas geholfen, aber es war immer viel Gefummel. Gerade, wenn der Code länger wurde. Seit einiger Zeit nutze ich chatGPT von openAI etwas intensiver und habe daher die Plus-Variante abonniert. Dort ist inzwischen codex enthalten. Codex ist ein AI-Assistent, der sich voll in IDEs integrieren lässt. In der Entwicklung von PHP-Anwendungen und Websites leistet er bereits gute Dienste – warum nicht auch für openHAB?

Mein Setup

openHAB läuft bei mir in einem Docker-Container im Synology Disk Station Manager. Die Ordner für Konfiguration, Logs usw. sind per Netzwerkshare zugänglich. Mein Visual Studio Workspace hat genau diese Ordner eingebunden, zusätzlich sind noch verschiedene Extensions, u.a. für openHAB installiert. Ebenso ist Codex eingerichtet.

Funktionsweise von Codex

Codex stellt ein Chatfenster in der IDE zur Verfügung, kann in verschiedener Hinsicht konfiguriert werden, in einer Sandbox laufen oder vollen Zugriff auf das Dateisystem haben, lässt sich auch remote mit github-Repos nutzen und bringt die jeweils aktuellen Sprachmodelle mit.

Ich habe mich entschieden, nicht jede Änderung manuell bestätigen zu wollen und habe codex Zugriff auf das Dateisystem gegeben. Hier müssen natürlich Risiken abgewogen werden. In ersten Versuchen abseits openHAB stellte ich dann z.B. fest, dass codex plötzlich anfing, Dateien außerhalb des Projektverzeichnisses zu ändern – was natürlich fatal ist. Ebenso ist nicht gut, wenn codex versucht auf Dateisystemebene json-Datenbanken etc. zu ändern. Hier kann viel schiefgehen und grundsätzlich sind Backups damit umso wichtiger. Generell kommt es auf die richtigen Prompts an und codex lässt sich auch leicht die richtigen Rahmenbedingungen mitgeben.

Werbung:

Die .codex/instructions.md

Für Rahmenbedingungen kann ein Verzeichnis „.codex“ und darin eine „instructions.md“ angelegt werden. Diese liest codex vor Start der Arbeit in einem Chat immer ein und berücksichtigt sie. Wichtig ist nun, hier die richtigen Rahmenbedingungen mitzugeben (Passwörter, Token / Zugangsdaten habe ich mit XXX unkenntlich gemacht).

Aktuell läuft openhab in einem docker container auf einer Synology Diskstation im Container Manager.

Du darfst außerhalb des Projektverzeichnisses U:\openhab4 nichts verändern und nur lesend auf nachfrage woanders auf Dateien zugreifen.

openhab läuft auf 192.168.1.10:8080
REST-Schnittstelle auf Port 192.168.1.10:8080/rest
user admin
passwort admin
API-Token (vscodeTokenCodex) für user admin: oh.vscodeTokenCodex.76vnlgezxJRXn787jZJo7zsmhG8MZeAnyQH32TxcYV2sidTEMZ49CyOWdiT4w6L09bMCowW3ZCph147l0bCA

Prüfe selbst welche Version von openhab läuft und lege all deinen Code auf diese Version aus. Zu älteren Versionen ist keine Kompatibilität erforderlich.

karaf konsole Port 8101
user openhab
passwort openab

Prüfe immer die Logausgaben, dir unter \userdata\logs liegen, auf Fehler nach dem ändern von rules und im Fehlerfall steuere gleich nach.

Prüfe den Zugriff über karaf console oder web-rest-api von openhab4 - nimm aber Veränderungen nur strickt nach Rücksprache vor (erstellen anpassen oder löschen von items / thinks / Konfigurationen/...)

Erstelle in dem vorhanden verzeichnis .codex eine changes.txt in welcher du alle Veränderungen protokollierst, die sich im Chatverlauf ergeben. Nur protokollieren, wenn Veränderungen an Dateien oder per karaf console oder REST API an Openhab vorgenommen wurden.

Wenn du Änderungen an JSON-Daten vornimmst nie direkt ins Dateisystem schreiben sondern nutze immer sauber Rest-API-Funktionen um aktuelles auszulesen Anpassungen im Speicher vorzunehmen und per Rest API wiederzuschreiben. ein .json - file darf nie verändert und geschrieben werden. Achte immer darauf, dass keine unerwünschte Nebenwirkungen eintreten.

Wenn Du Änderungen an Konfigurationsdateien vornimmst dann lass den vorherigenWwert immer drin stehen und Kommentiere ihn aus. Schreib über die dann neue zeile als Kommentar auch drüber von welchem wert auf welchen neuen eine Änderung vorgenommen wurde und warum. Setze vor jede Änderungsbeschreibung einen Datumzeitstempel in die Kommentierung.

Wenn Du Dateien schreiben musst, die nicht reguläre zur OpenHab INstanz gehören - z.B. temporäre json-Dateien für Widget-Erstellung usw lege diese bitte ausschließlich im .codex/temp - Ordner ab.
json-Daten in einen Unterordner darin "json". Wenn die json Daten änderst (natürlich per REST-API) speichere immer die jeweiligen Versionen als Historie ab (nummeriere einfach durch mit laufenden Nummern) so dass man leicht auch wieder mehrere Versionen zurück gehen kann, wenn nötig.

Mit diesen Rahmenbedingungen klappt das Arbeiten sehr gut. Nach Änderungen an Rules usw prüft er nun auch eigenständig logs und repariert mögliche Fehler automatisch.

Beispiel: Energieflussdiagramm in Minuten

Gerade in der Widgetprogrammierung mach KI Spaß und kann openHAB komplett per REST bedienen. Nachfolgend mein Beispiel für ein Widget zu Energieflüssen. Bitte nicht auf die Rechtschreibung achten, das habe ich einfach schnell reindiktiert bzw. schnell geschrieben – die KI korrigiert die Fehler ja eh 😉

Zuerst hat codex ein recht buntes Widget erstellt – ich möchte es aber eher schlank und im Stil meiner UI haben. Bei so etwas und auch bei der Fehlersuche allgemein lässt sich übrigens leicht einfach ein Screenshot in den Chat posten, der dann zur Analyse genutzt werden kann, wie im Chatverlauf auch zu sehen ist.

Werbung:

Chatverlauf

Das fertige Widget

Das Endergebnis nach ~ 10 Minuten im Hintergrund die KI arbeiten lassen lässt sich durchaus sehen:

Werbung:

Und hier der generierte Widget-Code

Widget Code
uid: strom_widget
tags:
- card
props:
parameters:
- description: Titel fuer das Stromfluss-Widget
label: Titel
name: propTitle
required: false
type: TEXT
parameterGroups: []
timestamp: Feb 13, 2026, 7:24:01 AM
component: f7-card
config:
title: "=props.propTitle ? props.propTitle : 'Stromfluss'"
style:
background: var(--f7-card-bg-color)
color: var(--f7-text-color)
border-radius: 18px
box-shadow: 0 12px 30px rgba(0,0,0,0.25)
overflow: hidden
--f7-button-text-color: var(--f7-text-color)
--f7-button-outline-text-color: var(--f7-text-color)
--f7-list-item-title-text-color: var(--f7-text-color)
--f7-list-item-after-text-color: var(--f7-text-color)
--f7-badge-text-color: var(--f7-text-color)
slots:
default:
- component: f7-block
config:
style:
margin: "0"
padding: 10px
display: grid
grid-template-columns: 1fr 0.35fr 1fr 0.35fr 1fr
grid-template-rows: auto auto auto auto auto
gap: 8px
align-items: center
slots:
default:
- component: f7-block
config:
style:
grid-column: "3"
grid-row: "1"
padding: 8px
margin: "0"
border-radius: 14px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.14))
text-align: center
slots:
default:
- component: oh-icon
config:
icon: f7:sun_max_fill
style:
font-size: 1.55rem
color: "#ffd54f"
- component: f7-block
config:
text: PV-Anlage
style:
font-size: 0.76rem
margin: 2px 0 0 0
padding: "0"
font-weight: "600"
- component: oh-button
config:
text: "=items.shellypro3empv_Power && items.shellypro3empv_Power.state ?
(Math.abs(Number.parseFloat(items.shellypro3empv_Power.st\
ate)).toFixed(0) + ' W') : 'n/a'"
action: analyzer
actionAnalyzerItems: shellypro3empv_Power
style:
width: 100%
height: 30px
font-size: 0.76rem
margin-top: 4px
border-radius: 10px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.16))
border: 1px solid var(--f7-list-outline-border-color, rgba(127,127,127,0.35))
color: var(--f7-text-color)
- component: f7-block
config:
style:
grid-column: "3"
grid-row: "2"
margin: "0"
padding: "0"
height: 36px
position: relative
slots:
default:
- component: f7-block
config:
style:
position: absolute
left: 50%
top: "0"
transform: translateX(-50%)
width: 1px
height: 100%
background: var(--f7-list-outline-border-color, rgba(127,127,127,0.38))
margin: "0"
padding: "0"
- component: oh-icon
config:
icon: f7:arrowtriangle_down_fill
visible: =(items.shellypro3empv_Power && items.shellypro3empv_Power.state &&
Math.abs(Number.parseFloat(items.shellypro3empv_Power.state))
> 20)
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
font-size: 2.45rem
color: "#ffe082"
- component: f7-block
config:
style:
grid-column: "1"
grid-row: "3"
padding: 8px
margin: "0"
border-radius: 14px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.14))
text-align: center
slots:
default:
- component: oh-icon
config:
icon: f7:bolt_horizontal_circle_fill
style:
font-size: 1.5rem
color: "#90caf9"
- component: f7-block
config:
text: Netz
style:
font-size: 0.76rem
margin: 2px 0 0 0
padding: "0"
font-weight: "600"
- component: oh-button
config:
text: "=items.strom_saldierend && items.strom_saldierend.state ?
((Number.parseFloat(items.strom_saldierend.state) >= 0 ?
'Bezug: ' : 'Einspeis.: ') +
Math.abs(Number.parseFloat(items.strom_saldierend.state))\
.toFixed(0) + ' W') : 'n/a'"
action: analyzer
actionAnalyzerItems: strom_saldierend
style:
width: 100%
height: 30px
font-size: 0.74rem
margin-top: 4px
border-radius: 10px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.16))
border: 1px solid var(--f7-list-outline-border-color, rgba(127,127,127,0.35))
color: var(--f7-text-color)
- component: f7-block
config:
style:
grid-column: "2"
grid-row: "3"
margin: "0"
padding: "0"
height: 100%
position: relative
slots:
default:
- component: f7-block
config:
style:
position: absolute
left: "0"
right: "0"
top: 50%
transform: translateY(-50%)
height: 1px
background: var(--f7-list-outline-border-color, rgba(127,127,127,0.38))
margin: "0"
padding: "0"
- component: oh-icon
config:
icon: f7:arrowtriangle_right_fill
visible: =(items.strom_saldierend && items.strom_saldierend.state &&
Number.parseFloat(items.strom_saldierend.state) > 20)
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
font-size: 2.45rem
color: "#a5d6a7"
- component: oh-icon
config:
icon: f7:arrowtriangle_left_fill
visible: =(items.strom_saldierend && items.strom_saldierend.state &&
Number.parseFloat(items.strom_saldierend.state) < -20)
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
font-size: 2.45rem
color: "#ef9a9a"
- component: f7-block
config:
style:
grid-column: "3"
grid-row: "3"
padding: 8px
margin: "0"
border-radius: 14px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.18))
text-align: center
slots:
default:
- component: oh-icon
config:
icon: f7:house_fill
style:
font-size: 1.5rem
color: "#ffcc80"
- component: f7-block
config:
text: Hausverbrauch
style:
font-size: 0.76rem
margin: 2px 0 0 0
padding: "0"
font-weight: "600"
- component: oh-button
config:
text: "=items.shellypro3emhaus_Power && items.shellypro3emhaus_Power.state ?
items.shellypro3emhaus_Power.state : 'n/a'"
action: analyzer
actionAnalyzerItems: shellypro3emhaus_Power
style:
width: 100%
height: 30px
font-size: 0.76rem
margin-top: 4px
border-radius: 10px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.16))
border: 1px solid var(--f7-list-outline-border-color, rgba(127,127,127,0.35))
color: var(--f7-text-color)
- component: f7-block
config:
style:
grid-column: "4"
grid-row: "3"
margin: "0"
padding: "0"
height: 100%
position: relative
slots:
default:
- component: f7-block
config:
style:
position: absolute
left: "0"
right: "0"
top: 50%
transform: translateY(-50%)
height: 1px
background: var(--f7-list-outline-border-color, rgba(127,127,127,0.38))
margin: "0"
padding: "0"
- component: oh-icon
config:
icon: f7:arrowtriangle_left_fill
visible: =(items.venus_battery_ac_power && items.venus_battery_ac_power.state &&
Number.parseFloat(items.venus_battery_ac_power.state) >
20)
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
font-size: 2.45rem
color: "#80deea"
- component: oh-icon
config:
icon: f7:arrowtriangle_right_fill
visible: =(items.venus_battery_ac_power && items.venus_battery_ac_power.state &&
Number.parseFloat(items.venus_battery_ac_power.state) <
-20)
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
font-size: 2.45rem
color: "#ffab91"
- component: f7-block
config:
style:
grid-column: "5"
grid-row: "3"
padding: 8px
margin: "0"
border-radius: 14px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.14))
text-align: center
slots:
default:
- component: f7-block
config:
style:
position: relative
width: 62px
height: 62px
margin: 0 auto 2px auto
border-radius: 50%
background: "= 'conic-gradient(#40c4ff ' + (items.venus_battery_soc &&
items.venus_battery_soc.state ? Math.max(0,
Math.min(100,
Number.parseFloat(items.venus_battery_soc.state))) : 0)
+ '%, rgba(255,255,255,0.14) 0)'"
slots:
default:
- component: f7-block
config:
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
width: 46px
height: 46px
border-radius: 50%
background: rgba(17,36,58,0.9)
display: flex
align-items: center
justify-content: center
font-size: 0.95rem
font-weight: "800"
color: "#ffffff"
text-shadow: 0 1px 2px rgba(0,0,0,0.55)
z-index: "3"
margin: "0"
padding: "0"
text: "=items.venus_battery_soc && items.venus_battery_soc.state ?
(Number.parseFloat(items.venus_battery_soc.state).t\
oFixed(0) + '%') : 'n/a'"
- component: f7-block
config:
text: Speicher
style:
font-size: 0.76rem
margin: "0"
padding: "0"
font-weight: "600"
- component: oh-button
config:
text: "=items.venus_battery_ac_power && items.venus_battery_ac_power.state ?
((Number.parseFloat(items.venus_battery_ac_power.state) >=
0 ? 'Entladen: ' : 'Laden: ') +
Math.abs(Number.parseFloat(items.venus_battery_ac_power.s\
tate)).toFixed(0) + ' W') : 'n/a'"
action: analyzer
actionAnalyzerItems: venus_battery_ac_power
style:
width: 100%
height: 30px
font-size: 0.72rem
margin-top: 4px
border-radius: 10px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.16))
border: 1px solid var(--f7-list-outline-border-color, rgba(127,127,127,0.35))
color: var(--f7-text-color)
- component: f7-block
config:
style:
grid-column: "3"
grid-row: "4"
margin: "0"
padding: "0"
height: 36px
position: relative
slots:
default:
- component: f7-block
config:
style:
position: absolute
left: 50%
top: "0"
transform: translateX(-50%)
width: 1px
height: 100%
background: var(--f7-list-outline-border-color, rgba(127,127,127,0.38))
margin: "0"
padding: "0"
- component: oh-icon
config:
icon: f7:arrowtriangle_down_fill
visible: =(items.GoeCharger_Power_All && items.GoeCharger_Power_All.state &&
Number.parseFloat(items.GoeCharger_Power_All.state) > 20)
style:
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
font-size: 2.45rem
color: "#80cbc4"
- component: f7-block
config:
style:
grid-column: "3"
grid-row: "5"
padding: 8px
margin: "0"
border-radius: 14px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.14))
text-align: center
slots:
default:
- component: oh-icon
config:
icon: f7:car_fill
style:
font-size: 1.55rem
color: "#a5d6a7"
- component: f7-block
config:
text: Wallbox
style:
font-size: 0.76rem
margin: 2px 0 0 0
padding: "0"
font-weight: "600"
- component: oh-button
config:
text: "=items.GoeCharger_Power_All && items.GoeCharger_Power_All.state ?
items.GoeCharger_Power_All.state : 'n/a'"
action: analyzer
actionAnalyzerItems: GoeCharger_Power_All
style:
width: 100%
height: 30px
font-size: 0.76rem
margin-top: 4px
border-radius: 10px
background: var(--f7-list-item-bg-color, rgba(127,127,127,0.16))
border: 1px solid var(--f7-list-outline-border-color, rgba(127,127,127,0.35))
color: var(--f7-text-color)

Eure Erfahrungen?

Was sind Eure Erfahrungen? Wie nutzt ihr KI mit openHAB? Nutzt gern die Kommentarspalte!

Eine Antwort auf „KI-gestützte openHAB-Entwicklung mit Visual Studio Code und openAI codex“

Schreibe einen Kommentar

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.

Durch die weitere Nutzung der Seite wird der Verwendung von Cookies und den Inhalten der Datenschutzerklärung zugestimmt. Weitere Informationen

Die Cookie-Einstellungen auf dieser Website sind auf "Cookies zulassen" eingestellt, um das beste Surferlebnis zu ermöglichen. Wenn du diese Website ohne Änderung der Cookie-Einstellungen verwendest oder auf "Akzeptieren" klickst, erklärst du sich damit einverstanden.

Schließen