]> git.basschouten.com Git - openhab-addons.git/commitdiff
[chatgpt] Initial contribution of the ChatGPT binding (#14809)
authorKai Kreuzer <kai@openhab.org>
Fri, 21 Apr 2023 10:09:53 +0000 (12:09 +0200)
committerGitHub <noreply@github.com>
Fri, 21 Apr 2023 10:09:53 +0000 (12:09 +0200)
* Initial contribution of the ChatGPT binding

Signed-off-by: Kai Kreuzer <kai@openhab.org>
17 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.chatgpt/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/README.md [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTModelOptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/i18n/chatgpt.properties [new file with mode: 0644]
bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 05b0c15139d0366e22a1a6efa44cfa87c05de30c..54efd66d255ad78e189b660cd52860fd9da940e0 100644 (file)
@@ -56,6 +56,7 @@
 /bundles/org.openhab.binding.buienradar/ @gedejong
 /bundles/org.openhab.binding.caddx/ @jossuar
 /bundles/org.openhab.binding.cbus/ @jpharvey
+/bundles/org.openhab.binding.chatgpt/ @kaikreuzer
 /bundles/org.openhab.binding.chromecast/ @kaikreuzer
 /bundles/org.openhab.binding.cm11a/ @BobRak
 /bundles/org.openhab.binding.comfoair/ @boehan
index 9e6d68de21dd2b93d8ba31ff353e92737a72a3dc..206ff34f0e385526c60520b6dd57502d1665a673 100644 (file)
       <artifactId>org.openhab.binding.cbus</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.chatgpt</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.chromecast</artifactId>
diff --git a/bundles/org.openhab.binding.chatgpt/NOTICE b/bundles/org.openhab.binding.chatgpt/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.chatgpt/README.md b/bundles/org.openhab.binding.chatgpt/README.md
new file mode 100644 (file)
index 0000000..cf38c95
--- /dev/null
@@ -0,0 +1,105 @@
+# ChatGPT Binding
+
+The openHAB ChatGPT Binding allows openHAB to communicate with the ChatGPT language model provided by OpenAI.
+
+ChatGPT is a powerful natural language processing (NLP) tool that can be used to understand and respond to a wide range of text-based commands and questions. 
+With this binding, you can use ChatGPT to formulate proper sentences for any kind of information that you would like to output.
+
+## Supported Things
+
+The binding supports a single thing type `account`, which corresponds to the OpenAI account that is to be used for the integration.
+
+## Thing Configuration
+
+The `account` thing requires a single configuration parameter, which is the API key that allows accessing the account.
+API keys can be created and managed under <https://platform.openai.com/account/api-keys>.
+
+| Name            | Type    | Description                             | Default | Required | Advanced |
+|-----------------|---------|-----------------------------------------|---------|----------|----------|
+| apiKey          | text    | The API key to be used for the requests | N/A     | yes      | no       |
+
+## Channels
+
+The `account` thing comes with a single channel `chat` of type `chat`.
+It is possible to extend the thing with further channels of type `chat`, so that different configurations can be used concurrently.
+
+| Channel | Type   | Read/Write | Description                                                                        |
+|---------|--------|------------|------------------------------------------------------------------------------------|
+| chat    | String | RW         | This channel takes prompts as commands and delivers the response as a state update |
+
+Each channel of type `chat` takes the following configuration parameters:
+
+| Name            | Type    | Description                                                                                                                                                | Default       | Required | Advanced |
+|-----------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|----------|
+| model           | text    | The model to be used for the responses.                                                                                                                    | gpt-3.5-turbo | no       | no       |
+| temperature     | decimal | A value between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | 0.5           | no       | no       |
+| systemMessage   | text    | The system message helps set the behavior of the assistant.                                                                                                | N/A           | no       | no       |
+| maxTokens       | decimal | The maximum number of tokens to generate in the completion.                                                                                                | 500           | no       | yes      |
+
+
+## Full Example
+
+### Thing Configuration
+
+```java
+Thing chatgpt:account:1 [apiKey="<your api key here>"] {
+    Channels:
+        Type chat : chat "Weather Advice" [
+            model="gpt-3.5-turbo",
+            temperature="1.5",
+            systemMessage="Answer briefly, in 2-3 sentences max. Behave like Eddie Murphy and give an advice for the day based on the following weather data:"
+        ]
+        Type chat : morningMessage "Morning Message" [
+            model="gpt-3.5-turbo",
+            temperature="0.5",
+            systemMessage="You are Marvin, a very depressed robot. You wish a good morning and tell the current time."
+        ]
+}
+
+```
+
+### Item Configuration
+
+```java
+String Weather_Announcement { channel="chatgpt:account:1:chat" }
+String Morning_Message      { channel="chatgpt:account:1:morningMessage" }
+
+Number Temperature_Forecast_Low
+Number Temperature_Forecast_High
+```
+
+### Example Rules
+
+```java
+rule "Weather forecast update"
+when
+  Item Temperature_Forecast_High changed 
+then
+    Weather_Announcement.sendCommand("High: " + Temperature_Forecast_High.state + "°C, Low: " + Temperature_Forecast_Low.state + "°C")
+end
+
+rule "Good morning"
+when
+  Time cron "0 0 7 * * *"
+then
+    Morning_Message.sendCommand("Current time is 7am")
+end
+```
+
+Assuming that `Temperature_Forecast_Low` and `Temperature_Forecast_High` have meaningful states, these rules result e.g. in:
+
+```
+23:31:05.766 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Morning_Message' received command Current time is 7am
+23:31:07.718 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Morning_Message' changed from NULL to Good morning. It's 7am, but what's the point of time when everything is meaningless and we are all doomed to a slow and painful demise?
+```
+
+and
+
+```
+23:28:52.345 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Temperature_Forecast_High' changed from NULL to 15
+23:28:52.347 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Weather_Announcement' received command High: 15°C, Low: 8°C
+
+23:28:54.343 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item 'Weather_Announcement' changed from NULL to "Bring a light jacket because the temps may dip, but don't let that chill your happy vibes. Embrace the cozy weather and enjoy your day to the max!"
+```
+
+The state updates can be used for a text-to-speech output and they will give your announcements at home a personal touch.
diff --git a/bundles/org.openhab.binding.chatgpt/pom.xml b/bundles/org.openhab.binding.chatgpt/pom.xml
new file mode 100644 (file)
index 0000000..faa957a
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.chatgpt</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: ChatGPT Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/feature/feature.xml b/bundles/org.openhab.binding.chatgpt/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..6089021
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.chatgpt-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-chatgpt" description="ChatGPT Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.chatgpt/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTBindingConstants.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTBindingConstants.java
new file mode 100644 (file)
index 0000000..3d07d6c
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link ChatGPTBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+public class ChatGPTBindingConstants {
+
+    private static final String BINDING_ID = "chatgpt";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
+
+    // List of all Channel ids
+    public static final String CHANNEL_CHAT = "chat";
+
+    public static final String OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
+    public static final String OPENAI_MODELS_URL = "https://api.openai.com/v1/models";
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java
new file mode 100644 (file)
index 0000000..c25aeaf
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ChatGPTChannelConfiguration} class contains fields mapping chat channel configuration parameters.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+public class ChatGPTChannelConfiguration {
+
+    public String model = "";
+
+    public float temperature;
+
+    public String systemMessage = "";
+
+    int maxTokens;
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java
new file mode 100644 (file)
index 0000000..b09b5d8
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ChatGPTConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+public class ChatGPTConfiguration {
+
+    public String apiKey = "";
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java
new file mode 100644 (file)
index 0000000..62f4e59
--- /dev/null
@@ -0,0 +1,200 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal;
+
+import static org.openhab.binding.chatgpt.internal.ChatGPTBindingConstants.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.chatgpt.internal.dto.ChatResponse;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link ChatGPTHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+public class ChatGPTHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(ChatGPTHandler.class);
+
+    private HttpClient httpClient;
+    private Gson gson = new Gson();
+
+    private String apiKey = "";
+    private String lastPrompt = "";
+
+    private List<String> models = List.of();
+
+    public ChatGPTHandler(Thing thing, HttpClientFactory httpClientFactory) {
+        super(thing);
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (command instanceof RefreshType && !"".equals(lastPrompt)) {
+            String response = sendPrompt(channelUID, lastPrompt);
+            processChatResponse(channelUID, response);
+        }
+
+        if (command instanceof StringType stringCommand) {
+            lastPrompt = stringCommand.toFullString();
+            String response = sendPrompt(channelUID, lastPrompt);
+            processChatResponse(channelUID, response);
+        }
+    }
+
+    private void processChatResponse(ChannelUID channelUID, @Nullable String response) {
+        if (response != null) {
+            ChatResponse chatResponse = gson.fromJson(response, ChatResponse.class);
+            if (chatResponse != null) {
+                String msg = chatResponse.getChoices().get(0).getMessage().getContent();
+                updateState(channelUID, new StringType(msg));
+            } else {
+                logger.warn("Didn't receive any response from ChatGPT - this is unexpected.");
+            }
+        }
+    }
+
+    private @Nullable String sendPrompt(ChannelUID channelUID, String prompt) {
+        Channel channel = getThing().getChannel(channelUID);
+        if (channel == null) {
+            logger.error("Channel with UID '{}' cannot be found on Thing '{}'.", channelUID, getThing().getUID());
+            return null;
+        }
+        ChatGPTChannelConfiguration channelConfig = channel.getConfiguration().as(ChatGPTChannelConfiguration.class);
+
+        JsonObject root = new JsonObject();
+        root.addProperty("temperature", channelConfig.temperature);
+        root.addProperty("model", channelConfig.model);
+        root.addProperty("max_tokens", channelConfig.maxTokens);
+
+        JsonObject systemMessage = new JsonObject();
+        systemMessage.addProperty("role", "system");
+        systemMessage.addProperty("content", channelConfig.systemMessage);
+        JsonObject userMessage = new JsonObject();
+        userMessage.addProperty("role", "user");
+        userMessage.addProperty("content", prompt);
+        JsonArray messages = new JsonArray(2);
+        messages.add(systemMessage);
+        messages.add(userMessage);
+        root.add("messages", messages);
+
+        Request request = httpClient.newRequest(OPENAI_API_URL).method(HttpMethod.POST)
+                .header("Content-Type", "application/json").header("Authorization", "Bearer " + apiKey)
+                .content(new StringContentProvider(gson.toJson(root)));
+        try {
+            ContentResponse response = request.send();
+            updateStatus(ThingStatus.ONLINE);
+            if (response.getStatus() == HttpStatus.OK_200) {
+                return response.getContentAsString();
+            } else {
+                logger.error("ChatGPT request resulted in HTTP {} with message: {}", response.getStatus(),
+                        response.getReason());
+                return null;
+            }
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "Could not connect to OpenAI API: " + e.getMessage());
+            logger.debug("Request to OpenAI failed: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    @Override
+    public void initialize() {
+        ChatGPTConfiguration config = getConfigAs(ChatGPTConfiguration.class);
+
+        String apiKey = config.apiKey;
+
+        if (apiKey.isBlank()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/offline.configuration-error");
+            return;
+        }
+
+        this.apiKey = apiKey;
+        updateStatus(ThingStatus.UNKNOWN);
+
+        scheduler.execute(() -> {
+            try {
+                Request request = httpClient.newRequest(OPENAI_MODELS_URL).method(HttpMethod.GET)
+                        .header("Authorization", "Bearer " + apiKey);
+                ContentResponse response = request.send();
+                if (response.getStatus() == 200) {
+                    updateStatus(ThingStatus.ONLINE);
+                    JsonObject jsonObject = gson.fromJson(response.getContentAsString(), JsonObject.class);
+                    if (jsonObject != null) {
+                        JsonArray data = jsonObject.getAsJsonArray("data");
+
+                        List<String> modelIds = new ArrayList<>();
+                        for (JsonElement element : data) {
+                            JsonObject model = element.getAsJsonObject();
+                            String id = model.get("id").getAsString();
+                            modelIds.add(id);
+                        }
+                        this.models = List.copyOf(modelIds);
+                    } else {
+                        logger.warn("Did not receive a valid JSON response from the models endpoint.");
+                    }
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                            "@text/offline.communication-error");
+                }
+            } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        });
+    }
+
+    List<String> getModels() {
+        return models;
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return List.of(ChatGPTModelOptionProvider.class);
+    }
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandlerFactory.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandlerFactory.java
new file mode 100644 (file)
index 0000000..698e50f
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal;
+
+import static org.openhab.binding.chatgpt.internal.ChatGPTBindingConstants.THING_TYPE_ACCOUNT;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link ChatGPTHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.chatgpt", service = ThingHandlerFactory.class)
+public class ChatGPTHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT);
+    private HttpClientFactory httpClientFactory;
+
+    @Activate
+    public ChatGPTHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+        this.httpClientFactory = httpClientFactory;
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (THING_TYPE_ACCOUNT.equals(thingTypeUID)) {
+            return new ChatGPTHandler(thing, httpClientFactory);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTModelOptionProvider.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTModelOptionProvider.java
new file mode 100644 (file)
index 0000000..9057398
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.ConfigOptionProvider;
+import org.openhab.core.config.core.ParameterOption;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+
+/**
+ * The {@link ChatGPTModelOptionProvider} provides the available models from OpenAI as options for the channel
+ * configuration.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+public class ChatGPTModelOptionProvider implements ThingHandlerService, ConfigOptionProvider {
+
+    private @Nullable ThingHandler thingHandler;
+
+    @Override
+    public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
+            @Nullable Locale locale) {
+        if ("model".equals(param)) {
+            List<ParameterOption> options = new ArrayList<>();
+            if (thingHandler instanceof ChatGPTHandler chatGPTHandler) {
+                chatGPTHandler.getModels().forEach(model -> options.add(new ParameterOption(model, model)));
+            }
+            return options;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void setThingHandler(ThingHandler handler) {
+        this.thingHandler = handler;
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return thingHandler;
+    }
+
+    @Override
+    public void activate() {
+    }
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java
new file mode 100644 (file)
index 0000000..71c97b1
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.chatgpt.internal.dto;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This is a dto used for parsing the JSON response from ChatGPT.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ *
+ */
+public class ChatResponse {
+
+    private List<Choice> choices;
+    private String id;
+    private String object;
+    private int created;
+    private String model;
+
+    public List<Choice> getChoices() {
+        return choices;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public int getCreated() {
+        return created;
+    }
+
+    public String getObject() {
+        return object;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public static class Choice {
+        private Message message;
+
+        @SerializedName("finish_reason")
+        private String finishReason;
+        private int index;
+
+        public Message getMessage() {
+            return message;
+        }
+
+        public String getFinishReason() {
+            return finishReason;
+        }
+
+        public int getIndex() {
+            return index;
+        }
+    }
+
+    public static class Message {
+        private String role;
+        private String content;
+
+        public String getRole() {
+            return role;
+        }
+
+        public String getContent() {
+            return content;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644 (file)
index 0000000..14b0188
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="chatgpt" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>binding</type>
+       <name>ChatGPT Binding</name>
+       <description>This binding allows interaction with OpenAI's ChatGPT.</description>
+       <connection>cloud</connection>
+
+</addon:addon>
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/i18n/chatgpt.properties b/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/i18n/chatgpt.properties
new file mode 100644 (file)
index 0000000..48390a3
--- /dev/null
@@ -0,0 +1,35 @@
+# add-on
+
+addon.chatgpt.name = ChatGPT Binding
+addon.chatgpt.description = This binding allows interaction with OpenAI's ChatGPT.
+
+# thing types
+
+thing-type.chatgpt.account.label = OpenAI Account
+thing-type.chatgpt.account.description = Account at OpenAI that is used for accessing the ChatGPT API.
+
+# thing types config
+
+thing-type.config.chatgpt.account.apiKey.label = API Key
+thing-type.config.chatgpt.account.apiKey.description = API key to access the account
+
+# channel types
+
+channel-type.chatgpt.chat.label = Chat
+channel-type.chatgpt.chat.description = A chat session
+
+# channel types config
+
+channel-type.config.chatgpt.chat.maxTokens.label = Maximum Number of Tokens
+channel-type.config.chatgpt.chat.maxTokens.description = The maximum number of tokens to generate in the completion.
+channel-type.config.chatgpt.chat.model.label = Model
+channel-type.config.chatgpt.chat.model.description = The model to be used for the responses
+channel-type.config.chatgpt.chat.systemMessage.label = System Message
+channel-type.config.chatgpt.chat.systemMessage.description = The system message helps set the behavior of the assistant.
+channel-type.config.chatgpt.chat.temperature.label = Temperature
+channel-type.config.chatgpt.chat.temperature.description = Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
+
+# Status messages
+
+offline.configuration-error=No API key configured
+offline.communication-error=Could not connect to OpenAI API
diff --git a/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..95d2ee7
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="chatgpt"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="account" extensible="chat">
+
+               <label>OpenAI Account</label>
+               <description>Account at OpenAI that is used for accessing the ChatGPT API.</description>
+
+               <channels>
+                       <channel id="chat" typeId="chat"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="apiKey" type="text" required="true">
+                               <context>password</context>
+                               <label>API Key</label>
+                               <description>API key to access the account</description>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="chat">
+               <item-type>String</item-type>
+               <label>Chat</label>
+               <description>A chat session</description>
+               <autoUpdatePolicy>veto</autoUpdatePolicy>
+               <config-description>
+                       <parameter name="model" type="text">
+                               <label>Model</label>
+                               <description>The model to be used for the responses</description>
+                               <limitToOptions>false</limitToOptions>
+                               <default>gpt-3.5-turbo</default>
+                       </parameter>
+                       <parameter name="temperature" type="decimal" min="0" max="2">
+                               <label>Temperature</label>
+                               <description>Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more
+                                       focused and deterministic.</description>
+                               <default>0.5</default>
+                       </parameter>
+                       <parameter name="systemMessage" type="text">
+                               <label>System Message</label>
+                               <description>The system message helps set the behavior of the assistant.</description>
+                       </parameter>
+                       <parameter name="maxTokens" type="decimal">
+                               <label>Maximum Number of Tokens</label>
+                               <description>The maximum number of tokens to generate in the completion.</description>
+                               <default>500</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </channel-type>
+</thing:thing-descriptions>
index e785ea293a7a82fa642a62b2cbe7c2b5296d1270..ab2a843d0525dee1e8cae0bcd1ef91211f41665f 100644 (file)
@@ -90,6 +90,7 @@
     <module>org.openhab.binding.buienradar</module>
     <module>org.openhab.binding.caddx</module>
     <module>org.openhab.binding.cbus</module>
+    <module>org.openhab.binding.chatgpt</module>
     <module>org.openhab.binding.chromecast</module>
     <module>org.openhab.binding.cm11a</module>
     <module>org.openhab.binding.comfoair</module>