diff --git a/CODEOWNERS b/CODEOWNERS
index 2755e0ac9e..daff67d58c 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -144,6 +144,7 @@
/bundles/org.openhab.binding.ftpupload/ @paulianttila
/bundles/org.openhab.binding.gardena/ @gerrieg @andrewfg
/bundles/org.openhab.binding.gce/ @clinique
+/bundles/org.openhab.binding.gemini/ @florian-h05
/bundles/org.openhab.binding.generacmobilelink/ @digitaldan
/bundles/org.openhab.binding.globalcache/ @mhilbush
/bundles/org.openhab.binding.goecharger/ @SamuelBrucksch
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 463d04bc68..65b60d788f 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -706,6 +706,11 @@
org.openhab.binding.gce${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.gemini
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.generacmobilelink
diff --git a/bundles/org.openhab.binding.gemini/NOTICE b/bundles/org.openhab.binding.gemini/NOTICE
new file mode 100644
index 0000000000..38d625e349
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/NOTICE
@@ -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.gemini/README.md b/bundles/org.openhab.binding.gemini/README.md
new file mode 100644
index 0000000000..55c3ac1235
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/README.md
@@ -0,0 +1,182 @@
+# Google Gemini Binding
+
+The openHAB Google Gemini Binding allows openHAB to communicate with the Gemini family of large language models (LLM) from Google.
+
+Google Gemini 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.
+This binding provides the following features:
+
+- Control openHAB with natural language commands: Turn on the lights, control the TV or check the status of the garage door.
+- Multi-Language Support: Issue commands and ask questions in almost any language.
+- Chat with a LLM: Ask questions and receive informational responses.
+
+## Supported Things
+
+The binding supports a single thing type `account`, which corresponds to the Google Gemini account that is to be used for the integration.
+
+## Discovery
+
+Discovery is not supported.
+Things must be added manually.
+
+## Thing Configuration
+
+The `account` thing requires the API key that allows accessing the account.
+API keys can be created and managed under Google AI Studio: .
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|------------------------------------------------------------------------------------------------------------------------------------------|------------------|----------|----------|
+| apiKey | text | The API key to authenticate against the Gemini API. | N/A | yes | no |
+| requestTimeout | integer | Timeout in seconds for chat API requests. | 30 | no | yes |
+| model | text | The model to be used ([Models](https://ai.google.dev/gemini-api/docs/models), [Pricing](https://ai.google.dev/gemini-api/docs/pricing)). | gemini-2.5-flash | no | no |
+| temperature | decimal | A value between 0.0 and 1.0, where higher values make the output more random and lower values make it more focused and deterministic. | 1.0 | no | yes |
+| topP | decimal | A value between 0.0 and 1.0 for nucleus sampling, where the model considers the results of the tokens with topP probability mass. | 1.0 | no | yes |
+| maxOutputTokens | integer | The maximum number of tokens to include in a candidate. | 2048 | no | yes |
+
+It is generally recommended to either alter temperature or topP, but not both.
+For Gemini 3.x models, Google recommends keeping both values at their default.
+
+## Channels
+
+The `account` thing comes with a single `chat` channel of type `chat` by default.
+Additional channels can be added to the thing, allowing specifying different models and parameters for each channel.
+
+Channels of type `chat` take the following configuration parameters:
+
+| Name | Type | Description | Required | Advanced |
+|-----------------|---------|------------------------------------------------------------------------------------------------|----------|----------|
+| model | text | The model to be used for the responses, overriding the thing-level model. | no | no |
+| systemMessage | text | The system message helps set the behavior of the assistant. | no | no |
+| temperature | decimal | A value between 0.0 and 2.0, which overrides the thing-level temperature. | no | yes |
+| topP | decimal | A value between 0.0 and 1.0, which overrides the thing-level topP. | no | yes |
+| maxOutputTokens | integer | The maximum number of tokens to include in a candidate, overriding the thing-level max tokens. | no | yes |
+
+Channel configuration defaults to the [Thing configuration](#thing-configuration), except for `systemMessage`, which defaults to _You are a helpful assistant_.
+
+## Thing Actions
+
+The binding provides actions to interact with the Google Gemini API directly from rules.
+The first parameter when retrieving actions must always be `gemini` and the second must be the full Thing UID of the Gemini account.
+
+You can retrieve the actions as follows:
+
+:::: tabs
+
+::: tab DSL
+
+```java
+val geminiActions = getActions("gemini", "gemini:account:myaccount")
+```
+
+:::
+
+::: tab JS
+
+```javascript
+var geminiActions = actions.thingActions("gemini", "gemini:account:myaccount");
+```
+
+:::
+
+::::
+
+### Available Actions
+
+The `account` Thing provides the following actions:
+
+| Action Signature | Return Type | Description |
+|----------------------------------------------------------------------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `sendMessage(String prompt)` | `String` | Sends a prompt to Gemini using the Thing's configured model and parameters, and returns the response text, or `null` if the request failed. |
+| `sendMessage(String prompt, String model)` | `String` | Sends a prompt to Gemini using the specified model, falling back to default parameters, and returns the response text, or `null` if the request failed. |
+| `sendMessage(String prompt, String model, String systemMessage, Double temperature, Double topP, Integer maxOutputTokens, Integer requestTimeout)` | `String` | Sends a prompt with detailed generation settings (where null values fall back to [Thing configuration](#thing-configuration)), and returns the response text, or `null` if the request failed. |
+
+### Examples
+
+:::: tabs
+
+::: tab DSL
+
+```java
+val geminiActions = getActions("gemini", "gemini:account:myaccount")
+
+// 1. Simple sendMessage
+val response1 = geminiActions.sendMessage("What is the capital of France?")
+logInfo("Gemini", "Response 1: " + response1)
+
+// 2. sendMessage with model override
+val response2 = geminiActions.sendMessage("Write a poem about openHAB.", "gemini-2.5-flash")
+logInfo("Gemini", "Response 2: " + response2)
+
+// 3. sendMessage with custom parameters (use null for default values)
+val response3 = geminiActions.sendMessage(
+ "How does electricity work?",
+ "gemini-2.5-flash",
+ "Explain it to a 5-year-old.", // system message
+ 0.7, // temperature
+ null, // topP (default)
+ 500, // maxOutputTokens
+ 60 // requestTimeout
+)
+logInfo("Gemini", "Response 3: " + response3)
+```
+
+:::
+
+::: tab JS
+
+```javascript
+const geminiActions = actions.thingActions("gemini", "gemini:account:myaccount");
+
+// 1. Simple sendMessage
+const response1 = geminiActions.sendMessage("What is the capital of France?");
+console.info("Gemini Response 1: " + response1);
+
+// 2. sendMessage with model override
+const response2 = geminiActions.sendMessage("Write a poem about openHAB.", "gemini-2.5-flash");
+console.info("Gemini Response 2: " + response2);
+
+// 3. sendMessage with custom parameters (use null for default values)
+const response3 = geminiActions.sendMessage(
+ "How does electricity work?",
+ "gemini-2.5-flash",
+ "Explain it to a 5-year-old.", // system message
+ 0.7, // temperature
+ null, // topP (default)
+ 500, // maxOutputTokens
+ 60 // requestTimeout
+);
+console.info("Gemini Response 3: " + response3);
+```
+
+:::
+
+::::
+
+## Human Language Interpreter
+
+The `account` Thing automatically registers a human language interpreter implementation.
+
+To configure the Gemini HLI as default, go to _Settings_ → _Voice_ and select _Gemini Human Language Interpreter_ as default.
+
+In that place, you can also configure the system prompt used to instruct the LLM on how to process the user's input.
+The used model, temperature, topP and maximum output tokens parameters can be configured in the thing configuration.
+
+For more information on human language interpreters, refer to the [Voice documentation]({{base}}/docs/configuration/multimedia.html#human-language-interpreter).
+
+## Full Example
+
+### Thing Configuration
+
+```java
+Thing gemini:account:myaccount [apiKey="xxx-yyy-zzz", model="gemini-2.5-flash", temperature=1.0, topP=1.0, maxOutputTokens=2048, requestTimeout=30] {
+ Channels:
+ Type chat : chat "Chat" [model="gemini-2.5-flash", temperature=1.0, topP=1.0, maxOutputTokens=2048, systemMessage="You are a helpful assistant."]
+}
+```
+
+Additional channels of type `chat` can be added to the thing.
+
+### Item Configuration
+
+```java
+String GeminiChat "Gemini Chat" { channel="gemini:account:myaccount:chat" }
+```
diff --git a/bundles/org.openhab.binding.gemini/pom.xml b/bundles/org.openhab.binding.gemini/pom.xml
new file mode 100644
index 0000000000..4917b347bf
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 5.2.0-SNAPSHOT
+
+
+ org.openhab.binding.gemini
+
+ openHAB Add-ons :: Bundles :: Gemini Binding
+
+
diff --git a/bundles/org.openhab.binding.gemini/src/main/feature/feature.xml b/bundles/org.openhab.binding.gemini/src/main/feature/feature.xml
new file mode 100644
index 0000000000..5def2be65e
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.gemini/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiBindingConstants.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiBindingConstants.java
new file mode 100644
index 0000000000..0255b18326
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiBindingConstants.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+
+/**
+ * The {@link GeminiBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+public class GeminiBindingConstants {
+ public static final String BINDING_ID = "gemini";
+
+ // 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 ChannelTypeUID CHANNEL_TYPE_UID_CHAT = new ChannelTypeUID(BINDING_ID, CHANNEL_CHAT);
+
+ // Default values for configuration parameters
+ public static final String DEFAULT_MODEL = "gemini-2.5-flash";
+ public static final double DEFAULT_TEMPERATURE = 1.0;
+ public static final double DEFAULT_TOP_P = 1.0;
+ public static final int DEFAULT_MAX_OUTPUT_TOKENS = 2048;
+ public static final String DEFAULT_SYSTEM_MESSAGE = "You are a helpful assistant.";
+ public static final int DEFAULT_REQUEST_TIMEOUT = 30;
+
+ // Default values as string needed for annotations
+ public static final String DEFAULT_TEMPERATURE_STR = "1.0";
+ public static final String DEFAULT_TOP_P_STR = "1.0";
+ public static final String MAX_OUTPUT_TOKENS_STR = "2048";
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiChannelConfiguration.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiChannelConfiguration.java
new file mode 100644
index 0000000000..3177beab0a
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiChannelConfiguration.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link GeminiChannelConfiguration} class contains the configuration for a Gemini chat channel.
+ * Values can be null/empty, in which case they fall back to the defaults from the thing-type.xml.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+public class GeminiChannelConfiguration {
+ public @Nullable String model;
+ public @Nullable Double temperature;
+ public @Nullable Double topP;
+ public @Nullable Integer maxOutputTokens;
+ public @Nullable String systemMessage;
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiConfiguration.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiConfiguration.java
new file mode 100644
index 0000000000..24db08a957
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiConfiguration.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_MAX_OUTPUT_TOKENS;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_MODEL;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_REQUEST_TIMEOUT;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_TEMPERATURE;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_TOP_P;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GeminiConfiguration} class contains the configuration for the Gemini account thing.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+public class GeminiConfiguration {
+ // Authentication
+ public String apiKey = "";
+ // Connection
+ public int requestTimeout = DEFAULT_REQUEST_TIMEOUT;
+ // HLI
+ public String model = DEFAULT_MODEL;
+ public double temperature = DEFAULT_TEMPERATURE;
+ public double topP = DEFAULT_TOP_P;
+ public int maxOutputTokens = DEFAULT_MAX_OUTPUT_TOKENS;
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiHandler.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiHandler.java
new file mode 100644
index 0000000000..034cd0b2aa
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiHandler.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.gemini.internal.action.GeminiActions;
+import org.openhab.binding.gemini.internal.api.GeminiApiClient;
+import org.openhab.binding.gemini.internal.api.GeminiApiException;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiModel;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
+import org.openhab.binding.gemini.internal.hli.GeminiHLIService;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link GeminiHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+public class GeminiHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(GeminiHandler.class);
+
+ private final HttpClient httpClient;
+ private @Nullable GeminiConfiguration config;
+ private @Nullable GeminiApiClient apiClient;
+ private List models = List.of();
+ private @Nullable ScheduledFuture> initFuture;
+
+ public GeminiHandler(Thing thing, HttpClientFactory httpClientFactory) {
+ super(thing);
+ this.httpClient = httpClientFactory.getCommonHttpClient();
+ }
+
+ public @Nullable GeminiConfiguration getGeminiConfiguration() {
+ return this.config;
+ }
+
+ public @Nullable GeminiApiClient getApiClient() {
+ return apiClient;
+ }
+
+ public List getModels() {
+ return models;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ Channel channel = getThing().getChannel(channelUID);
+ if (channel != null && CHANNEL_TYPE_UID_CHAT.equals(channel.getChannelTypeUID())
+ && command instanceof StringType stringCommand) {
+ String lastPrompt = stringCommand.toFullString();
+
+ GeminiConfiguration config = this.config;
+ GeminiApiClient client = apiClient;
+ if (client != null) {
+ int timeout = config != null ? config.requestTimeout : DEFAULT_REQUEST_TIMEOUT;
+ String model = (config != null && !config.model.isBlank()) ? config.model : DEFAULT_MODEL;
+ double temp = config != null ? config.temperature : DEFAULT_TEMPERATURE;
+ double topP = config != null ? config.topP : DEFAULT_TOP_P;
+ int maxTokens = config != null ? config.maxOutputTokens : DEFAULT_MAX_OUTPUT_TOKENS;
+ String systemMessage = DEFAULT_SYSTEM_MESSAGE;
+
+ GeminiChannelConfiguration channelConfig = channel.getConfiguration()
+ .as(GeminiChannelConfiguration.class);
+
+ String channelModel = channelConfig.model;
+ if (channelModel != null && !channelModel.isBlank()) {
+ model = channelModel;
+ }
+
+ Double channelTemp = channelConfig.temperature;
+ if (channelTemp != null) {
+ temp = channelTemp;
+ }
+
+ Double channelTopP = channelConfig.topP;
+ if (channelTopP != null) {
+ topP = channelTopP;
+ }
+
+ Integer channelMaxTokens = channelConfig.maxOutputTokens;
+ if (channelMaxTokens != null) {
+ maxTokens = channelMaxTokens;
+ }
+
+ String channelSystemMessage = channelConfig.systemMessage;
+ if (channelSystemMessage != null && !channelSystemMessage.isBlank()) {
+ systemMessage = channelSystemMessage;
+ }
+
+ try {
+ GeminiResponse response = client.sendPrompt(model, lastPrompt, systemMessage, temp, topP, maxTokens,
+ timeout);
+ processChatResponse(channelUID, response);
+ updateStatus(ThingStatus.ONLINE);
+ } catch (GeminiApiException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Could not connect to Gemini API: " + e.getMessage());
+ logger.debug("Request to Gemini failed: {}", e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ private void processChatResponse(ChannelUID channelUID, GeminiResponse response) {
+ String msg = response.getFirstText();
+ if (msg != null) {
+ updateState(channelUID, new StringType(msg));
+ } else {
+ logger.warn("Didn't receive any chat response candidates from Gemini - this is unexpected.");
+ }
+ }
+
+ @Override
+ public void initialize() {
+ ScheduledFuture> future = initFuture;
+ if (future != null) {
+ future.cancel(true);
+ this.initFuture = null;
+ }
+
+ final GeminiConfiguration c = getConfigAs(GeminiConfiguration.class);
+ this.config = c;
+
+ String apiKey = c.apiKey;
+
+ if (apiKey.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.configuration-error");
+ return;
+ }
+
+ if (invalidTimeout(c.requestTimeout)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/requestTimeout.configuration-error");
+ return;
+ }
+
+ this.apiClient = new GeminiApiClient(httpClient, apiKey);
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ this.initFuture = scheduler.schedule(() -> {
+ try {
+ GeminiApiClient client = this.apiClient;
+ GeminiConfiguration currentConfig = this.config;
+ if (client == null || currentConfig == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.configuration-error");
+ return;
+ }
+ List apiModels = client.fetchModels(currentConfig.requestTimeout);
+ if (!apiModels.isEmpty()) {
+ List modelList = new ArrayList<>();
+ Set seenNames = new HashSet<>();
+ for (GeminiModel model : apiModels) {
+ String name = model.name();
+ if (name != null) {
+ List methods = model.supportedGenerationMethods();
+ if (methods != null && methods.contains("generateContent")) {
+ String cleanName = name.replace("models/", "");
+ String lowerName = cleanName.toLowerCase(Locale.ROOT);
+ if (lowerName.startsWith("gemini") || lowerName.startsWith("gemma")) {
+ if (seenNames.add(cleanName)) {
+ modelList.add(model);
+ }
+ }
+ }
+ }
+ }
+ this.models = List.copyOf(modelList);
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.communication-error");
+ }
+ } catch (GeminiApiException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }, 0, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> future = initFuture;
+ if (future != null) {
+ future.cancel(true);
+ this.initFuture = null;
+ }
+ super.dispose();
+ }
+
+ @Override
+ public Collection> getServices() {
+ return List.of(GeminiModelOptionProvider.class, GeminiActions.class, GeminiHLIService.class);
+ }
+
+ private boolean invalidTimeout(int timeout) {
+ return timeout <= 0;
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiHandlerFactory.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiHandlerFactory.java
new file mode 100644
index 0000000000..c86c87800b
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiHandlerFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.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 GeminiHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.gemini", service = ThingHandlerFactory.class)
+public class GeminiHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT);
+ private HttpClientFactory httpClientFactory;
+
+ @Activate
+ public GeminiHandlerFactory(@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 GeminiHandler(thing, httpClientFactory);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiModelOptionProvider.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiModelOptionProvider.java
new file mode 100644
index 0000000000..8d3967ab74
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/GeminiModelOptionProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.CHANNEL_TYPE_UID_CHAT;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.THING_TYPE_ACCOUNT;
+
+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;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ServiceScope;
+
+/**
+ * The {@link GeminiModelOptionProvider} provides the available models from Gemini as configuration options.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@Component(scope = ServiceScope.PROTOTYPE, service = { GeminiModelOptionProvider.class, ConfigOptionProvider.class })
+@NonNullByDefault
+public class GeminiModelOptionProvider implements ThingHandlerService, ConfigOptionProvider {
+ private @Nullable ThingHandler thingHandler;
+
+ @Override
+ public void setThingHandler(ThingHandler handler) {
+ this.thingHandler = handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return thingHandler;
+ }
+
+ @Override
+ public @Nullable Collection getParameterOptions(URI uri, String param, @Nullable String context,
+ @Nullable Locale locale) {
+ if (CHANNEL_TYPE_UID_CHAT.getAsString().equals(uri.getSchemeSpecificPart())
+ || THING_TYPE_ACCOUNT.getAsString().equals(uri.getSchemeSpecificPart())) {
+ if ("model".equals(param)) {
+ List options = new ArrayList<>();
+ if (thingHandler instanceof GeminiHandler geminiHandler) {
+ geminiHandler.getModels().forEach(model -> {
+ String name = model.name();
+ if (name != null) {
+ String cleanName = name.replace("models/", "");
+ String displayName = model.displayName();
+ String label = displayName != null ? displayName : cleanName;
+ options.add(new ParameterOption(cleanName, label));
+ }
+ });
+ }
+ return options;
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/action/GeminiActions.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/action/GeminiActions.java
new file mode 100644
index 0000000000..936aca1119
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/action/GeminiActions.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.action;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.*;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gemini.internal.GeminiConfiguration;
+import org.openhab.binding.gemini.internal.GeminiHandler;
+import org.openhab.binding.gemini.internal.api.GeminiApiClient;
+import org.openhab.binding.gemini.internal.api.GeminiApiException;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ServiceScope;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link GeminiActions} class provides Thing Actions for the Gemini binding.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@Component(scope = ServiceScope.PROTOTYPE, service = GeminiActions.class)
+@ThingActionsScope(name = "gemini")
+@NonNullByDefault
+public class GeminiActions implements ThingActions {
+
+ private final Logger logger = LoggerFactory.getLogger(GeminiActions.class);
+
+ private @Nullable GeminiHandler handler;
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof GeminiHandler geminiHandler) {
+ this.handler = geminiHandler;
+ } else {
+ this.handler = null;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+
+ public @Nullable String sendMessage(@Nullable String prompt) {
+ return sendMessage(prompt, null);
+ }
+
+ public @Nullable String sendMessage(@Nullable String prompt, @Nullable String model) {
+ return sendMessage(prompt, model, null, null, null, null, null);
+ }
+
+ @RuleAction(label = "@text/action.sendMessage.label", description = "@text/action.sendMessage.description")
+ public @Nullable @ActionOutput(label = "@text/action.sendMessage.result.label", description = "@text/action.sendMessage.result.description", type = "java.lang.String") String sendMessage(
+ @ActionInput(name = "prompt", label = "@text/action.sendMessage.prompt.label", description = "@text/action.sendMessage.prompt.description", type = "java.lang.String", required = true) @Nullable String prompt,
+ @ActionInput(name = "model", label = "@text/action.sendMessage.model.label", description = "@text/action.sendMessage.model.description", type = "java.lang.String") @Nullable String model,
+ @ActionInput(name = "systemMessage", label = "@text/action.sendMessage.systemMessage.label", description = "@text/action.sendMessage.systemMessage.description", type = "java.lang.String") @Nullable String systemMessage,
+ @ActionInput(name = "temperature", label = "@text/action.sendMessage.temperature.label", description = "@text/action.sendMessage.temperature.description", type = "java.lang.Double") @Nullable Double temperature,
+ @ActionInput(name = "topP", label = "@text/action.sendMessage.topP.label", description = "@text/action.sendMessage.topP.description", type = "java.lang.Double") @Nullable Double topP,
+ @ActionInput(name = "maxOutputTokens", label = "@text/action.sendMessage.maxOutputTokens.label", description = "@text/action.sendMessage.maxOutputTokens.description", type = "java.lang.Integer") @Nullable Integer maxOutputTokens,
+ @ActionInput(name = "requestTimeout", label = "@text/action.sendMessage.requestTimeout.label", description = "@text/action.sendMessage.requestTimeout.description", type = "java.lang.Integer") @Nullable Integer requestTimeout) {
+ if (prompt == null || prompt.isBlank()) {
+ logger.warn("Cannot send message: prompt is null or blank.");
+ return null;
+ }
+
+ GeminiHandler geminiHandler = handler;
+ if (geminiHandler == null) {
+ logger.warn("Cannot send message: Gemini handler is not initialized.");
+ return null;
+ }
+ GeminiApiClient apiClient = geminiHandler.getApiClient();
+ if (apiClient == null) {
+ logger.warn("Cannot send message: Gemini API client is not initialized.");
+ return null;
+ }
+
+ GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
+
+ String resolvedModel = (model != null && !model.isBlank()) ? model
+ : ((config != null && !config.model.isBlank()) ? config.model : DEFAULT_MODEL);
+ String resolvedSystemMessage = Objects.requireNonNullElse(systemMessage, DEFAULT_SYSTEM_MESSAGE);
+ double resolvedTemperature = Objects.requireNonNullElse(temperature,
+ config != null ? config.temperature : DEFAULT_TEMPERATURE);
+ double resolvedTopP = Objects.requireNonNullElse(topP, config != null ? config.topP : DEFAULT_TOP_P);
+ int resolvedMaxOutputTokens = Objects.requireNonNullElse(maxOutputTokens,
+ config != null ? config.maxOutputTokens : DEFAULT_MAX_OUTPUT_TOKENS);
+ int resolvedRequestTimeout = Objects.requireNonNullElse(requestTimeout,
+ config != null ? config.requestTimeout : DEFAULT_REQUEST_TIMEOUT);
+
+ try {
+ GeminiResponse response = apiClient.sendPrompt(resolvedModel, prompt, resolvedSystemMessage,
+ resolvedTemperature, resolvedTopP, resolvedMaxOutputTokens, resolvedRequestTimeout);
+ String text = response.getFirstText();
+ if (text != null) {
+ return text;
+ }
+ logger.warn("Didn't receive any chat response candidates from Gemini - this is unexpected.");
+ } catch (GeminiApiException e) {
+ logger.debug("Request to Gemini via action failed: {}", e.getMessage(), e);
+ }
+ return null;
+ }
+
+ public static @Nullable String sendMessage(ThingActions actions, @Nullable String prompt) {
+ if (!(actions instanceof GeminiActions geminiActions)) {
+ throw new IllegalArgumentException("The 'actions' argument is not an instance of GeminiActions");
+ }
+ return geminiActions.sendMessage(prompt);
+ }
+
+ public static @Nullable String sendMessage(ThingActions actions, @Nullable String prompt, @Nullable String model) {
+ if (!(actions instanceof GeminiActions geminiActions)) {
+ throw new IllegalArgumentException("The 'actions' argument is not an instance of GeminiActions");
+ }
+ return geminiActions.sendMessage(prompt, model);
+ }
+
+ public static @Nullable String sendMessage(ThingActions actions, @Nullable String prompt, @Nullable String model,
+ @Nullable String systemMessage, @Nullable Double temperature, @Nullable Double topP,
+ @Nullable Integer maxOutputTokens, @Nullable Integer requestTimeout) {
+ if (!(actions instanceof GeminiActions geminiActions)) {
+ throw new IllegalArgumentException("The 'actions' argument is not an instance of GeminiActions");
+ }
+
+ return geminiActions.sendMessage(prompt, model, systemMessage, temperature, topP, maxOutputTokens,
+ requestTimeout);
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/GeminiApiClient.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/GeminiApiClient.java
new file mode 100644
index 0000000000..95202a52d0
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/GeminiApiClient.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_REQUEST_TIMEOUT;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+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.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.openhab.binding.gemini.internal.api.dto.GeminiContent;
+import org.openhab.binding.gemini.internal.api.dto.GeminiFunctionCall;
+import org.openhab.binding.gemini.internal.api.dto.GeminiPart;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiFunctionDeclaration;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiFunctionResponse;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiGenerationConfig;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiRequest;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiSchema;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiTool;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiModel;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiModelsResponse;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
+import org.openhab.core.voice.text.conversation.Conversation;
+import org.openhab.core.voice.text.interpreter.llm.LLMTool;
+import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
+import org.openhab.core.voice.text.interpreter.llm.LLMToolParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * The {@link GeminiApiClient} class encapsulates all HTTP/REST API communications with the Google Gemini API.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+public class GeminiApiClient {
+ private static final String GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
+ private static final String HEADER_API_KEY = "x-goog-api-key";
+ private static final String ROLE_USER = "user";
+ private static final String ROLE_MODEL = "model";
+
+ private final Logger logger = LoggerFactory.getLogger(GeminiApiClient.class);
+
+ private final HttpClient httpClient;
+ private final String apiKey;
+ private final ObjectMapper objectMapper;
+
+ public GeminiApiClient(HttpClient httpClient, String apiKey) {
+ this.httpClient = httpClient;
+ this.apiKey = apiKey;
+ this.objectMapper = new ObjectMapper();
+ this.objectMapper.setDefaultPropertyInclusion(
+ com.fasterxml.jackson.annotation.JsonInclude.Value.construct(Include.NON_NULL, Include.ALWAYS));
+ }
+
+ /**
+ * Sends a single prompt to the Gemini API and returns the response DTO.
+ *
+ * @param model the Gemini model to use
+ * @param prompt the prompt text
+ * @param systemMessage the system instruction text
+ * @param temperature the temperature config parameter
+ * @param topP the topP config parameter
+ * @param maxOutputTokens the maxOutputTokens config parameter
+ * @param timeoutSeconds request timeout in seconds
+ * @return the deserialized GeminiResponse
+ * @throws GeminiApiException if a communication error, timeout, or parsing error occurs
+ */
+ public GeminiResponse sendPrompt(String model, String prompt, @Nullable String systemMessage,
+ @Nullable Double temperature, @Nullable Double topP, @Nullable Integer maxOutputTokens,
+ @Nullable Integer timeoutSeconds) throws GeminiApiException {
+ GeminiContent systemInstruction = createSystemInstruction(systemMessage);
+
+ // Contents
+ GeminiPart userPart = new GeminiPart(prompt, null, null, null);
+ GeminiContent userContent = new GeminiContent(ROLE_USER, List.of(userPart));
+
+ // Config
+ GeminiGenerationConfig genConfig = new GeminiGenerationConfig(maxOutputTokens, temperature, topP, null);
+
+ GeminiRequest request = new GeminiRequest(List.of(userContent), systemInstruction, genConfig, null);
+
+ return executeGenerateContentRequest(model, request, timeoutSeconds);
+ }
+
+ /**
+ * Sends a conversation history and tools to the Gemini API and returns the response DTO.
+ *
+ * @param model the Gemini model to use
+ * @param history the conversation message history
+ * @param tools the available tools
+ * @param systemMessage the system instruction text
+ * @param temperature the temperature config parameter
+ * @param topP the topP config parameter
+ * @param maxOutputTokens the maxOutputTokens config parameter
+ * @param timeoutSeconds request timeout in seconds
+ * @return the deserialized GeminiResponse
+ * @throws GeminiApiException if a communication error, timeout, or parsing error occurs
+ */
+ public GeminiResponse sendPrompt(String model, List history, List tools,
+ @Nullable String systemMessage, @Nullable Double temperature, @Nullable Double topP,
+ @Nullable Integer maxOutputTokens, @Nullable Integer timeoutSeconds) throws GeminiApiException {
+ GeminiContent systemInstruction = createSystemInstruction(systemMessage);
+
+ List contents = new ArrayList<>();
+ Queue pendingToolCallNames = new LinkedList<>();
+
+ for (Conversation.Message msg : history) {
+ switch (msg.role()) {
+ case USER: {
+ GeminiPart part = new GeminiPart(msg.content(), null, null, null);
+ contents.add(new GeminiContent(ROLE_USER, List.of(part)));
+ break;
+ }
+ case OPENHAB: {
+ GeminiPart part = new GeminiPart(msg.content(), null, null, null);
+ contents.add(new GeminiContent(ROLE_MODEL, List.of(part)));
+ break;
+ }
+ case TOOL_CALL: {
+ LLMToolCall toolCall = LLMToolCall.fromJson(msg.content());
+ String name = toolCall.tool().replaceAll("[^a-zA-Z0-9_-]", "_");
+ pendingToolCallNames.add(name);
+ GeminiFunctionCall fc = new GeminiFunctionCall(name, toolCall.params());
+ GeminiPart part = new GeminiPart(null, fc, null, null);
+ contents.add(new GeminiContent(ROLE_MODEL, List.of(part)));
+ break;
+ }
+ case TOOL_RETURN: {
+ String name = pendingToolCallNames.poll();
+ if (name == null) {
+ name = "";
+ }
+ GeminiFunctionResponse fr = new GeminiFunctionResponse(name, Map.of("result", msg.content()));
+ GeminiPart part = new GeminiPart(null, null, fr, null);
+ contents.add(new GeminiContent(ROLE_USER, List.of(part)));
+ break;
+ }
+ case THINKING:
+ break;
+ }
+ }
+
+ List geminiTools = null;
+ if (!tools.isEmpty()) {
+ List functions = new ArrayList<>();
+ for (LLMTool t : tools) {
+ Map properties = new HashMap<>();
+ List required = new ArrayList<>();
+
+ for (LLMToolParam p : t.getParamDescriptions(null)) {
+ List enumValues = p.options().isEmpty() ? null : p.options();
+ String typeStr = p.type().name().toLowerCase(Locale.ROOT);
+ GeminiSchema itemsSchema = null;
+ if ("array".equals(typeStr)) {
+ itemsSchema = new GeminiSchema("string");
+ }
+ GeminiSchema prop = new GeminiSchema(typeStr, p.description(), null, null, enumValues, itemsSchema);
+ properties.put(p.name(), prop);
+ if (p.required()) {
+ required.add(p.name());
+ }
+ }
+
+ GeminiSchema params = new GeminiSchema("object", null, properties, required.isEmpty() ? null : required,
+ null, null);
+
+ String fName = t.getUID().replaceAll("[^a-zA-Z0-9_-]", "_");
+ String fDesc = t.getDescription(null);
+ functions.add(new GeminiFunctionDeclaration(fName, fDesc, params));
+ }
+ geminiTools = List.of(new GeminiTool(functions));
+ }
+
+ GeminiGenerationConfig genConfig = new GeminiGenerationConfig(maxOutputTokens, temperature, topP, null);
+
+ GeminiRequest request = new GeminiRequest(contents, systemInstruction, genConfig, geminiTools);
+
+ return executeGenerateContentRequest(model, request, timeoutSeconds);
+ }
+
+ /**
+ * Executes an HTTP POST request to the generateContent endpoint of a Gemini model.
+ *
+ *
+ * If the request fails with 503 Service Unavailable, up to two retries are performed with with an increasing delay.
+ *
+ * @param model the Gemini model to use
+ * @param requestPayload the payload to send
+ * @param timeoutSeconds request timeout in seconds
+ * @return the deserialized GeminiResponse
+ * @throws GeminiApiException if a communication error, timeout, or parsing error occurs
+ */
+ private GeminiResponse executeGenerateContentRequest(String model, GeminiRequest requestPayload,
+ @Nullable Integer timeoutSeconds) throws GeminiApiException {
+ String url = GEMINI_API_BASE_URL + "/models/" + model + ":generateContent";
+
+ String queryJson;
+ try {
+ queryJson = objectMapper.writeValueAsString(requestPayload);
+ } catch (JsonProcessingException e) {
+ throw new GeminiApiException("Failed to serialize Gemini request: " + e.getMessage(), e);
+ }
+
+ int attemptCount = 1;
+ while (true) {
+ Request request = httpClient.newRequest(url).method(HttpMethod.POST)
+ .timeout((Objects.requireNonNullElse(timeoutSeconds, DEFAULT_REQUEST_TIMEOUT)), TimeUnit.SECONDS)
+ .header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON.asString())
+ .header(HEADER_API_KEY, apiKey)
+ .content(new StringContentProvider(queryJson, StandardCharsets.UTF_8));
+ if (logger.isDebugEnabled()) {
+ try {
+ String prettyJson = objectMapper.writerWithDefaultPrettyPrinter()
+ .writeValueAsString(requestPayload);
+ logger.debug("Request to {} (attempt {}): \n{}", url, attemptCount, prettyJson);
+ } catch (JsonProcessingException e) {
+ logger.debug("Request to {} (attempt {}): {}", url, attemptCount, queryJson);
+ }
+ }
+
+ try {
+ ContentResponse response = request.send();
+ if (response.getStatus() == HttpStatus.OK_200) {
+ String responseBody = response.getContentAsString();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Response from {}: {}", url, responseBody);
+ }
+ try {
+ @Nullable
+ GeminiResponse geminiResponse = readNullableValue(responseBody, GeminiResponse.class);
+ if (geminiResponse == null) {
+ throw new GeminiApiException("Failed to parse Gemini response: response was null");
+ }
+ return geminiResponse;
+ } catch (JsonProcessingException e) {
+ throw new GeminiApiException("Failed to parse Gemini response: " + e.getMessage(), e);
+ }
+ } else if (response.getStatus() == HttpStatus.SERVICE_UNAVAILABLE_503 && attemptCount <= 3) {
+ logger.debug("Gemini request failed with 503 Service Unavailable on attempt #{}/3", attemptCount);
+ try {
+ Thread.sleep(1000 * attemptCount);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new GeminiApiException(
+ "Interrupted while waiting to retry Gemini API request: " + e.getMessage(), e);
+ }
+ attemptCount++;
+ } else {
+ logger.debug("Gemini request failed on the final attempt with HTTP {} {}: {}", response.getStatus(),
+ response.getReason(), response.getContentAsString());
+ throw new GeminiApiException("Gemini generateContent request resulted failed with HTTP "
+ + response.getStatus() + " " + response.getReason());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new GeminiApiException("Could not connect to Gemini API: " + e.getMessage(), e);
+ } catch (TimeoutException | ExecutionException e) {
+ throw new GeminiApiException("Could not connect to Gemini API: " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Fetches the list of models supported by the Gemini API key.
+ *
+ * @param timeoutSeconds request timeout in seconds
+ * @return a list of GeminiModel objects
+ * @throws GeminiApiException if a communication error, timeout, or parsing error occurs
+ */
+ public List fetchModels(@Nullable Integer timeoutSeconds) throws GeminiApiException {
+ String modelsUrl = GEMINI_API_BASE_URL + "/models";
+ Request request = httpClient.newRequest(modelsUrl)
+ .timeout(Objects.requireNonNullElse(timeoutSeconds, DEFAULT_REQUEST_TIMEOUT), TimeUnit.SECONDS)
+ .method(HttpMethod.GET).header(HEADER_API_KEY, apiKey);
+ logger.debug("Request to {}: (GET)", modelsUrl);
+
+ try {
+ ContentResponse response = request.send();
+ if (response.getStatus() == HttpStatus.OK_200) {
+ String responseBody = response.getContentAsString();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Response from {}: {}", modelsUrl, responseBody);
+ }
+ try {
+ @Nullable
+ GeminiModelsResponse modelsResponse = readNullableValue(responseBody, GeminiModelsResponse.class);
+ if (modelsResponse == null) {
+ throw new GeminiApiException("Failed to parse models response DTO: response was null");
+ }
+ List models = modelsResponse.models();
+ if (models != null) {
+ return models;
+ }
+ } catch (JsonProcessingException e) {
+ throw new GeminiApiException("Failed to parse models response DTO: " + e.getMessage(), e);
+ }
+ } else {
+ throw new GeminiApiException(
+ "Gemini request for models resulted in HTTP status " + response.getStatus());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new GeminiApiException("Could not retrieve models from Gemini API: " + e.getMessage(), e);
+ } catch (TimeoutException | ExecutionException e) {
+ throw new GeminiApiException("Could not retrieve models from Gemini API: " + e.getMessage(), e);
+ }
+ return new ArrayList<>();
+ }
+
+ private @Nullable GeminiContent createSystemInstruction(@Nullable String systemMessage) {
+ if (systemMessage != null && !systemMessage.isBlank()) {
+ GeminiPart sysPart = new GeminiPart(systemMessage, null, null, null);
+ return new GeminiContent(null, List.of(sysPart));
+ }
+ return null;
+ }
+
+ private @Nullable T readNullableValue(String content, Class valueType) throws JsonProcessingException {
+ return objectMapper.readValue(content, valueType);
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/GeminiApiException.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/GeminiApiException.java
new file mode 100644
index 0000000000..750101e06f
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/GeminiApiException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api;
+
+import java.io.Serial;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GeminiApiException} class represents an exception thrown during communication
+ * with the Google Gemini API.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@NonNullByDefault
+public class GeminiApiException extends Exception {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public GeminiApiException(String message) {
+ super(message);
+ }
+
+ public GeminiApiException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiContent.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiContent.java
new file mode 100644
index 0000000000..87abbd6768
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiContent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiContent} record represents the structured content of a message in Gemini API requests
+ * and responses, associating a role (e.g., "user" or "model") with a list of content parts.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiContent(@Nullable String role, @Nullable List parts) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiFunctionCall.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiFunctionCall.java
new file mode 100644
index 0000000000..a0d3bd731a
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiFunctionCall.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiFunctionCall} record represents a function call request generated by the model,
+ * indicating the function name and arguments to be executed.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiFunctionCall(@Nullable String name, @Nullable Map args) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiPart.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiPart.java
new file mode 100644
index 0000000000..85fe602455
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/GeminiPart.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gemini.internal.api.dto.request.GeminiFunctionResponse;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiPart} record represents a single part of content, which may contain text, a function call,
+ * or a function response.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiPart(@Nullable String text, @Nullable GeminiFunctionCall functionCall,
+ @Nullable GeminiFunctionResponse functionResponse, @Nullable Boolean thought) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiFunctionDeclaration.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiFunctionDeclaration.java
new file mode 100644
index 0000000000..caf3f57816
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiFunctionDeclaration.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiFunctionDeclaration} record defines a tool's function signature, description,
+ * and parameters schema that the model can request to execute.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiFunctionDeclaration(@Nullable String name, @Nullable String description,
+ @Nullable GeminiSchema parameters) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiFunctionResponse.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiFunctionResponse.java
new file mode 100644
index 0000000000..81de25c134
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiFunctionResponse.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiFunctionResponse} record represents the execution result of a function call
+ * sent back to the model as context.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiFunctionResponse(@Nullable String name, @Nullable Map response) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiGenerationConfig.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiGenerationConfig.java
new file mode 100644
index 0000000000..d9028324fb
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiGenerationConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiGenerationConfig} record contains the configuration parameters for controlling
+ * content generation, such as temperature, topP, and max output tokens.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiGenerationConfig(@Nullable Integer maxOutputTokens, @Nullable Double temperature,
+ @Nullable Double topP, @Nullable GeminiThinkingConfig thinkingConfig) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiRequest.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiRequest.java
new file mode 100644
index 0000000000..881e41734e
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiRequest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gemini.internal.api.dto.GeminiContent;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiRequest} record represents the payload for a content generation request, including
+ * chat history, system instructions, tools, and generation configurations.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiRequest(@Nullable List contents, @Nullable GeminiContent systemInstruction,
+ @Nullable GeminiGenerationConfig generationConfig, @Nullable List tools) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiSchema.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiSchema.java
new file mode 100644
index 0000000000..0d71aa9581
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiSchema.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * The {@link GeminiSchema} record represents a JSON schema definition for tool parameters and properties.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(Include.NON_NULL)
+@NonNullByDefault
+public record GeminiSchema(String type, @Nullable String description, @Nullable Map properties,
+ @Nullable List required, @JsonProperty("enum") @Nullable List enumValues,
+ @Nullable GeminiSchema items) {
+
+ public GeminiSchema(String type) {
+ this(type, null, null, null, null, null);
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiThinkingConfig.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiThinkingConfig.java
new file mode 100644
index 0000000000..0291537c83
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiThinkingConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiThinkingConfig} record contains the parameters for controlling the thinking behavior
+ * of Gemini models that support reasoning.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiThinkingConfig(@Nullable Boolean includeThoughts, @Nullable Integer thinkingBudget,
+ @Nullable String thinkingLevel) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiTool.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiTool.java
new file mode 100644
index 0000000000..a1707f15b2
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/request/GeminiTool.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.request;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiTool} record defines the list of functions/declarations made available to the model.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiTool(@Nullable List functionDeclarations) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiCandidate.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiCandidate.java
new file mode 100644
index 0000000000..679158c374
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiCandidate.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.response;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gemini.internal.api.dto.GeminiContent;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiCandidate} record represents a single candidate generation/response from the Gemini model.
+ * It contains the generated content and the reason generation finished.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiCandidate(@Nullable GeminiContent content, @Nullable String finishReason) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiModel.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiModel.java
new file mode 100644
index 0000000000..248fb8c6ac
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiModel.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.response;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiModel} record represents details of a model returned from the models list endpoint.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiModel(@Nullable String name, @Nullable String version, @Nullable String displayName,
+ @Nullable String description, @Nullable List supportedGenerationMethods) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiModelsResponse.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiModelsResponse.java
new file mode 100644
index 0000000000..cfd420659c
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiModelsResponse.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.response;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiModelsResponse} record represents the response payload containing the list of available
+ * Gemini models.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiModelsResponse(@Nullable List models) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiResponse.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiResponse.java
new file mode 100644
index 0000000000..5aba865651
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiResponse.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.response;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiResponse} record represents the response payload for a content generation request,
+ * containing the candidates generated by the model and usage metadata.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiResponse(@Nullable List candidates, @Nullable GeminiUsageMetadata usageMetadata) {
+
+ public @Nullable String getFirstText() {
+ List candidatesList = candidates;
+ if (candidatesList != null && !candidatesList.isEmpty()) {
+ GeminiCandidate candidate = candidatesList.getFirst();
+ var content = candidate.content();
+ if (content != null) {
+ var parts = content.parts();
+ if (parts != null && !parts.isEmpty()) {
+ var part = parts.getFirst();
+ return part.text();
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiUsageMetadata.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiUsageMetadata.java
new file mode 100644
index 0000000000..356a95c340
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/api/dto/response/GeminiUsageMetadata.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.api.dto.response;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@link GeminiUsageMetadata} record represents token usage metadata for the request and response.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@NonNullByDefault
+public record GeminiUsageMetadata(@Nullable Integer promptTokenCount, @Nullable Integer candidatesTokenCount,
+ @Nullable Integer totalTokenCount, @Nullable Integer thoughtsTokenCount) {
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/hli/GeminiHLIService.java b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/hli/GeminiHLIService.java
new file mode 100644
index 0000000000..bac15615ea
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/java/org/openhab/binding/gemini/internal/hli/GeminiHLIService.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2010-2026 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.gemini.internal.hli;
+
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.BINDING_ID;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_MODEL;
+import static org.openhab.binding.gemini.internal.GeminiBindingConstants.DEFAULT_SYSTEM_MESSAGE;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gemini.internal.GeminiConfiguration;
+import org.openhab.binding.gemini.internal.GeminiHandler;
+import org.openhab.binding.gemini.internal.api.GeminiApiClient;
+import org.openhab.binding.gemini.internal.api.GeminiApiException;
+import org.openhab.binding.gemini.internal.api.dto.GeminiContent;
+import org.openhab.binding.gemini.internal.api.dto.GeminiFunctionCall;
+import org.openhab.binding.gemini.internal.api.dto.GeminiPart;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiCandidate;
+import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.voice.text.HumanLanguageInterpreter;
+import org.openhab.core.voice.text.InterpretationException;
+import org.openhab.core.voice.text.InterpreterContext;
+import org.openhab.core.voice.text.conversation.Conversation;
+import org.openhab.core.voice.text.conversation.ConversationException;
+import org.openhab.core.voice.text.conversation.ConversationRole;
+import org.openhab.core.voice.text.interpreter.llm.LLMTool;
+import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
+import org.openhab.core.voice.text.interpreter.llm.LLMToolException;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link GeminiHLIService} is a {@link HumanLanguageInterpreter} implementation based on Google Gemini.
+ *
+ * @author Florian Hotze - Initial contribution
+ */
+@Component(service = { GeminiHLIService.class, HumanLanguageInterpreter.class })
+@NonNullByDefault
+public class GeminiHLIService implements ThingHandlerService, HumanLanguageInterpreter {
+ private static final String LABEL = "Gemini Human Language Interpreter";
+
+ private @Nullable GeminiHandler handler;
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ if (handler instanceof GeminiHandler geminiHandler) {
+ this.handler = geminiHandler;
+ } else {
+ this.handler = null;
+ }
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return handler;
+ }
+
+ @Override
+ public String getId() {
+ return BINDING_ID;
+ }
+
+ @Override
+ public String getLabel(@Nullable Locale locale) {
+ return LABEL;
+ }
+
+ @Override
+ public Set getSupportedLocales() {
+ return Set.of();
+ }
+
+ @Override
+ public Set getSupportedGrammarFormats() {
+ return Set.of();
+ }
+
+ @Override
+ public @Nullable String getGrammar(Locale locale, String format) {
+ return null;
+ }
+
+ @Override
+ public String interpret(Locale locale, String text) throws InterpretationException {
+ GeminiHandler geminiHandler = handler;
+ if (geminiHandler == null) {
+ throw new InterpretationException("GeminiHandler is not initialized");
+ }
+ GeminiApiClient apiClient = geminiHandler.getApiClient();
+ if (apiClient == null) {
+ throw new InterpretationException("Gemini API client is not available");
+ }
+ GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
+ if (config == null) {
+ throw new InterpretationException("Gemini HLI configuration is not available");
+ }
+ String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
+ try {
+ GeminiResponse geminiResponse = apiClient.sendPrompt(model, text, DEFAULT_SYSTEM_MESSAGE,
+ config.temperature, config.topP, config.maxOutputTokens, config.requestTimeout);
+ String response = geminiResponse.getFirstText();
+ if (response != null) {
+ return response;
+ }
+ } catch (GeminiApiException e) {
+ var ex = new InterpretationException("Failed to get response from Gemini: " + e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ }
+ throw new InterpretationException("No valid response received");
+ }
+
+ @Override
+ public String interpret(Locale locale, InterpreterContext interpreterContext) throws InterpretationException {
+ GeminiHandler geminiHandler = handler;
+ if (geminiHandler == null) {
+ throw new InterpretationException("GeminiHandler is not initialized");
+ }
+ GeminiApiClient apiClient = geminiHandler.getApiClient();
+ if (apiClient == null) {
+ throw new InterpretationException("Gemini API client is not available");
+ }
+
+ GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
+ if (config == null) {
+ throw new InterpretationException("Gemini HLI configuration is not available");
+ }
+
+ Conversation conversation = interpreterContext.conversation();
+ List tools = interpreterContext.tools();
+
+ String systemMessage = interpreterContext.systemPrompt();
+ if (systemMessage == null || systemMessage.isBlank()) {
+ throw new InterpretationException("System prompt is missing or empty");
+ }
+ String locationItem = interpreterContext.locationItem();
+ if (locationItem != null && !locationItem.isBlank()) {
+ systemMessage = systemMessage.trim() + "\n\n" + "Your location (item name): " + locationItem;
+ }
+ if (!tools.isEmpty()) {
+ String toolGuidance = """
+ You have tools available to interact with the environment. Use them when appropriate.
+ However, if the user's request cannot be fulfilled by any tool, or if they ask a general question,
+ answer them directly using your general knowledge.
+ Do not try to force a tool call. Do not state that you can only perform actions supported by the tools.
+ When using a tool, always provide a clear and concise natural language response.
+ """;
+ systemMessage = systemMessage.trim() + "\n\n" + toolGuidance;
+ }
+
+ int loopCount = 0;
+ final int maxLoops = 10;
+ while (true) {
+ if (loopCount >= maxLoops) {
+ throw new InterpretationException(
+ "Tool execution loop limit exceeded (max " + maxLoops + " iterations)");
+ }
+ loopCount++;
+ String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
+ try {
+ GeminiResponse geminiResponse = apiClient.sendPrompt(model, conversation.getMessages(), tools,
+ systemMessage, config.temperature, config.topP, config.maxOutputTokens, config.requestTimeout);
+
+ List candidates = geminiResponse.candidates();
+ if (candidates == null || candidates.isEmpty()) {
+ throw new InterpretationException("No valid response received");
+ }
+ GeminiContent responseContent = candidates.getFirst().content();
+ if (responseContent == null) {
+ throw new InterpretationException("No valid response received");
+ }
+ List parts = responseContent.parts();
+ if (parts == null) {
+ throw new InterpretationException("No valid response received");
+ }
+
+ boolean hasToolCall = false;
+ StringBuilder textBuilder = new StringBuilder();
+
+ for (GeminiPart part : parts) {
+ GeminiFunctionCall fc = part.functionCall();
+ if (fc != null) {
+ hasToolCall = true;
+ String toolName = fc.name();
+ Map args = fc.args();
+
+ LLMToolCall llmToolCall = new LLMToolCall(toolName != null ? toolName : "",
+ args != null ? args : new HashMap<>());
+ try {
+ conversation.addMessage(ConversationRole.TOOL_CALL, llmToolCall.toJson());
+ } catch (ConversationException e) {
+ var ex = new InterpretationException("Failed to add TOOL_CALL to conversation");
+ ex.initCause(e);
+ throw ex;
+ }
+
+ String result = executeTool(tools, toolName, args, locale);
+
+ try {
+ conversation.addMessage(ConversationRole.TOOL_RETURN, result);
+ } catch (ConversationException e) {
+ var ex = new InterpretationException("Failed to add TOOL_RETURN to conversation");
+ ex.initCause(e);
+ throw ex;
+ }
+ } else if (part.text() != null) {
+ textBuilder.append(part.text());
+ }
+ }
+
+ if (!hasToolCall) {
+ String finalResponse = textBuilder.toString();
+ try {
+ conversation.addMessage(ConversationRole.OPENHAB, finalResponse);
+ } catch (ConversationException e) {
+ var ex = new InterpretationException("Failed to add OPENHAB message to conversation");
+ ex.initCause(e);
+ throw ex;
+ }
+ return finalResponse;
+ }
+ } catch (GeminiApiException e) {
+ InterpretationException ex = new InterpretationException(
+ "Failed to communicate with Gemini: " + e.getMessage());
+ ex.initCause(e);
+ throw ex;
+ }
+ }
+ }
+
+ private String executeTool(List tools, @Nullable String toolName, @Nullable Map args,
+ Locale locale) {
+ if (toolName == null) {
+ return "Error: Tool name is null";
+ }
+ LLMTool tool = tools.stream().filter(t -> t.getUID().replaceAll("[^a-zA-Z0-9_-]", "_").equals(toolName))
+ .findFirst().orElse(null);
+ if (tool == null) {
+ return "Error: Tool " + toolName + " not found";
+ }
+ try {
+ return tool.call(args != null ? args : new HashMap<>(), locale);
+ } catch (LLMToolException e) {
+ return "Error: " + e.getMessage();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000..ec2ae7ebb4
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,11 @@
+
+
+
+ binding
+ Gemini Binding
+ Integrates with the Google Gemini API for AI-powered chat and smart home control.
+ cloud
+
+
diff --git a/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/i18n/gemini.properties b/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/i18n/gemini.properties
new file mode 100644
index 0000000000..bd5569e6a3
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/i18n/gemini.properties
@@ -0,0 +1,73 @@
+# add-on
+
+addon.gemini.name = Gemini Binding
+addon.gemini.description = Integrates with the Google Gemini API for AI-powered chat and smart home control.
+
+# thing types
+
+thing-type.gemini.account.label = Gemini Account
+thing-type.gemini.account.description = Integrates the Google Gemini API with openHAB.
+
+# thing types config
+
+thing-type.config.gemini.account.apiKey.label = API Key
+thing-type.config.gemini.account.apiKey.description = The Google Gemini API key (Create & Manage API keys in Google AI Studio).
+thing-type.config.gemini.account.group.authentication.label = Authentication
+thing-type.config.gemini.account.group.connection.label = Connection
+thing-type.config.gemini.account.group.hli.label = Model Configuration
+thing-type.config.gemini.account.group.hli.description = Configures the used model and its behavior. Applies to the Human Language Interpreter and as a default for all channels and Thing actions.
+thing-type.config.gemini.account.maxOutputTokens.label = Max Output Tokens
+thing-type.config.gemini.account.maxOutputTokens.description = The maximum number of tokens to generate in the response.
+thing-type.config.gemini.account.model.label = Model
+thing-type.config.gemini.account.model.description = The Google Gemini model to use (Models, Pricing).
+thing-type.config.gemini.account.requestTimeout.label = Request Timeout
+thing-type.config.gemini.account.requestTimeout.description = The timeout in seconds for API requests.
+thing-type.config.gemini.account.temperature.label = Temperature
+thing-type.config.gemini.account.temperature.description = Controls randomness. Higher values mean more random responses.
+thing-type.config.gemini.account.topP.label = Top-P
+thing-type.config.gemini.account.topP.description = Controls the cumulative probability threshold for token selection.
+
+# channel types
+
+channel-type.gemini.chat.label = Chat
+channel-type.gemini.chat.description = Send a prompt to a model and receive a response.
+
+# channel types config
+
+channel-type.config.gemini.chat.maxOutputTokens.label = Max Output Tokens
+channel-type.config.gemini.chat.maxOutputTokens.description = The maximum number of tokens to generate in the response.
+channel-type.config.gemini.chat.model.label = Model
+channel-type.config.gemini.chat.model.description = The Gemini model to use.
+channel-type.config.gemini.chat.systemMessage.label = System Message
+channel-type.config.gemini.chat.systemMessage.description = The system instruction provided to the model.
+channel-type.config.gemini.chat.temperature.label = Temperature
+channel-type.config.gemini.chat.temperature.description = Controls randomness. Higher values mean more random responses.
+channel-type.config.gemini.chat.topP.label = Top-P
+channel-type.config.gemini.chat.topP.description = Controls the cumulative probability threshold for token selection.
+
+# actions
+
+action.sendMessage.label = Send Message
+action.sendMessage.description = Sends a message to Gemini and returns the response.
+action.sendMessage.prompt.label = Prompt
+action.sendMessage.prompt.description = The prompt message to send to Gemini.
+action.sendMessage.model.label = Model
+action.sendMessage.model.description = The specific Gemini model to use for this message.
+action.sendMessage.systemMessage.label = System Message
+action.sendMessage.systemMessage.description = The system instruction provided to the model.
+action.sendMessage.temperature.label = Temperature
+action.sendMessage.temperature.description = The temperature setting to control randomness.
+action.sendMessage.topP.label = Top P
+action.sendMessage.topP.description = Controls the cumulative probability threshold for token selection.
+action.sendMessage.maxOutputTokens.label = Max Output Tokens
+action.sendMessage.maxOutputTokens.description = The maximum number of tokens to generate in the response.
+action.sendMessage.requestTimeout.label = Request Timeout
+action.sendMessage.requestTimeout.description = The timeout in seconds for this request.
+action.sendMessage.result.label = Response
+action.sendMessage.result.description = The text response returned by Gemini.
+
+# thing status
+
+offline.configuration-error = No API key configured
+requestTimeout.configuration-error = Timeout must be greater than zero
+offline.communication-error = Could not connect to Gemini API
diff --git a/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000..d3ede6776f
--- /dev/null
+++ b/bundles/org.openhab.binding.gemini/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Integrates the Google Gemini API with openHAB.
+
+
+
+
+
+
+
+ network-security
+
+
+
+
+ Configures the used model and its behavior. Applies to the Human Language Interpreter and as a default
+ for all channels and Thing actions.
+
+
+ network
+
+
+
+
+
+ password
+
+
+ Google AI Studio).
+ ]]>
+
+
+
+
+
+
+ Models, Pricing).
+ ]]>
+
+ gemini-2.5-flash
+
+
+
+ Controls randomness. Higher values mean more random responses.
+ 1.0
+ true
+
+
+
+ Controls the cumulative probability threshold for token selection.
+ 1.0
+ true
+
+
+
+ The maximum number of tokens to generate in the response.
+ 2048
+ true
+
+
+
+
+ The timeout in seconds for API requests.
+ 30
+ true
+
+
+
+
+
+ String
+
+ Send a prompt to a model and receive a response.
+ veto
+
+
+
+ The Gemini model to use.
+
+
+
+ Controls randomness. Higher values mean more random responses.
+ true
+
+
+
+ Controls the cumulative probability threshold for token selection.
+ true
+
+
+
+ The maximum number of tokens to generate in the response.
+ true
+
+
+
+ The system instruction provided to the model.
+ You are a helpful assistant.
+ false
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index d5c468f816..32901b3823 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -181,6 +181,7 @@
org.openhab.binding.ftpuploadorg.openhab.binding.gardenaorg.openhab.binding.gce
+ org.openhab.binding.geminiorg.openhab.binding.generacmobilelinkorg.openhab.binding.goechargerorg.openhab.binding.govee