diff --git a/CODEOWNERS b/CODEOWNERS
index b3a4a596800..0b2fbdf12c9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -65,6 +65,7 @@
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
/bundles/org.openhab.binding.buienradar/ @gedejong
/bundles/org.openhab.binding.caddx/ @jossuar
+/bundles/org.openhab.binding.casokitchen/ @weymann
/bundles/org.openhab.binding.cbus/ @jpharvey
/bundles/org.openhab.binding.chatgpt/ @kaikreuzer
/bundles/org.openhab.binding.chromecast/ @kaikreuzer
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index bc172373256..0c2158d9fd2 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -311,6 +311,11 @@
org.openhab.binding.caddx
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.casokitchen
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.cbus
diff --git a/bundles/org.openhab.binding.casokitchen/NOTICE b/bundles/org.openhab.binding.casokitchen/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/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.casokitchen/README.md b/bundles/org.openhab.binding.casokitchen/README.md
new file mode 100644
index 00000000000..37c91835ad1
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/README.md
@@ -0,0 +1,86 @@
+# CasoKitchen Binding
+
+Provides access towards CASO Smart Kitchen devices which are connected within the [CASO Control App](https://www.casocontrol.de/).
+
+## Supported Things
+
+- `winecooler-2z`: Wine cooler with two zones
+
+## Discovery
+
+There's no automatic discovery.
+
+## Thing Configuration
+
+You need a [CASO Account](https://www.casoapp.com/Account/Create) to get configuration parameters.
+After register you'll get the
+
+- API key
+- Device ID
+
+## Wine Cooler with 2 Zones
+
+### Configuration winecooler-2z
+
+| Name | Type | Description | Default |
+|-----------------|---------|------------------------------------------------------|---------|
+| apiKey | text | API obtained from thing configuration | N/A |
+| deviceId | text | Device Id obtained from thing configuration | N/A |
+| refreshInterval | integer | Interval the device is polled in minutes | 5 |
+
+### Channels winecooler-2z
+
+Channels are separated in 3 groups
+
+- `generic` group covering states for the whole device
+- `top` and `bottom` group covering states related to top or bottom zone
+
+#### Generic Group
+
+Group name `generic`.
+
+| Channel | Type | Read/Write | Description |
+|---------------|----------|------------|------------------------------|
+| light-switch | Switch | RW | Control lights for all zones |
+| last-update | DateTime | R | Date and Time of last update |
+| hint | String | R | General command description |
+
+#### Zone Groups
+
+Group `top` and `bottom`.
+
+The `set-temperature` channel is holding the desired temperature controlled via buttons on the wine cooler device.
+Currently it cannot be changed using the API.
+
+| Channel | Type | Read/Write | Description |
+|------------------|-----------------------|------------|------------------------------|
+| power | Switch | R | Zone Power |
+| temperature | Number:Temperature | R | Current Zone Temperature |
+| set-temperature | Number:Temperature | R | Desired Zone Temperature |
+| light-switch | Switch | RW | Control lights for this zone |
+
+## Full Example
+
+### Thing Configuration
+
+```java
+Thing casokitchen:winecooler-2z:whiny "Whiny Wine Cooler" [ apiKey="ABC", deviceId="XYZ" ]
+```
+
+### Item Configuration
+
+```java
+Switch Whiny_Generic_LightSwitch {channel="casokitchen:winecooler-2z:whiny:generic#light-switch" }
+DateTime Whiny_Generic_LastUpdate {channel="casokitchen:winecooler-2z:whiny:generic#last-update" }
+String Whiny_Generic_Hint {channel="casokitchen:winecooler-2z:whiny:generic#hint" }
+
+Switch Whiny_Top_Power {channel="casokitchen:winecooler-2z:whiny:top#power" }
+Number:Temperature Whiny_Top_CurrentTemperature {channel="casokitchen:winecooler-2z:whiny:top#temperature" }
+Number:Temperature Whiny_Top_DesiredTemperature {channel="casokitchen:winecooler-2z:whiny:top#set-temperature" }
+Switch Whiny_Top_LightSwitch {channel="casokitchen:winecooler-2z:whiny:top#light-switch" }
+
+Switch Whiny_Bottom_Power {channel="casokitchen:winecooler-2z:whiny:bottom#power" }
+Number:Temperature Whiny_Bottom_CurrentTemperature {channel="casokitchen:winecooler-2z:whiny:bottom#temperature" }
+Number:Temperature Whiny_Bottom_DesiredTemperature {channel="casokitchen:winecooler-2z:whiny:bottom#set-temperature" }
+Switch Whiny_Bottom_LightSwitch {channel="casokitchen:winecooler-2z:whiny:bottom#light-switch" }
+```
diff --git a/bundles/org.openhab.binding.casokitchen/pom.xml b/bundles/org.openhab.binding.casokitchen/pom.xml
new file mode 100644
index 00000000000..497a5690b98
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 5.0.0-SNAPSHOT
+
+
+ org.openhab.binding.casokitchen
+
+ openHAB Add-ons :: Bundles :: CasoKitchen Binding
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/feature/feature.xml b/bundles/org.openhab.binding.casokitchen/src/main/feature/feature.xml
new file mode 100644
index 00000000000..bcdbbbd598d
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/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.casokitchen/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/CasoKitchenBindingConstants.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/CasoKitchenBindingConstants.java
new file mode 100644
index 00000000000..3cbf8fb71f9
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/CasoKitchenBindingConstants.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link CasoKitchenBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class CasoKitchenBindingConstants {
+ private static final String BINDING_ID = "casokitchen";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_WINECOOLER = new ThingTypeUID(BINDING_ID, "winecooler-2z");
+
+ // List of all Channel Group ids
+ public static final String TOP = "top";
+ public static final String BOTTOM = "bottom";
+ public static final String GENERIC = "generic";
+
+ // List of all Channel ids
+ public static final String TEMPERATURE = "temperature";
+ public static final String TARGET_TEMPERATURE = "set-temperature";
+ public static final String POWER = "power";
+ public static final String LIGHT = "light-switch";
+ public static final String HINT = "hint";
+ public static final String LAST_UPDATE = "last-update";
+
+ public static final int MINIMUM_REFRESH_INTERVAL_MIN = 5;
+ public static final String EMPTY = "";
+
+ public static final String BASE_URL = "https://publickitchenapi.casoapp.com";
+ public static final String LIGHT_URL = BASE_URL + "/api/v1.1/Winecooler/SetLight";
+ public static final String STATUS_URL = BASE_URL + "/api/v1.1/Winecooler/Status";
+ public static final String HTTP_HEADER_API_KEY = "x-api-key";
+
+ public static final Gson GSON = new Gson();
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/CasoKitchenHandlerFactory.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/CasoKitchenHandlerFactory.java
new file mode 100644
index 00000000000..7855a480c43
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/CasoKitchenHandlerFactory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal;
+
+import static org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants.THING_TYPE_WINECOOLER;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.casokitchen.internal.handler.TwoZonesWinecoolerHandler;
+import org.openhab.core.i18n.TimeZoneProvider;
+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 CasoKitchenHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.casokitchen", service = ThingHandlerFactory.class)
+public class CasoKitchenHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_WINECOOLER);
+ private HttpClientFactory httpClientFactory;
+ private TimeZoneProvider timeZoneProvider;
+
+ @Activate
+ public CasoKitchenHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
+ final @Reference TimeZoneProvider tzp) {
+ this.httpClientFactory = httpClientFactory;
+ timeZoneProvider = tzp;
+ }
+
+ @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_WINECOOLER.equals(thingTypeUID)) {
+ return new TwoZonesWinecoolerHandler(thing, httpClientFactory.getCommonHttpClient(), timeZoneProvider);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/config/TwoZonesWinecoolerConfiguration.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/config/TwoZonesWinecoolerConfiguration.java
new file mode 100644
index 00000000000..eff861a5a57
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/config/TwoZonesWinecoolerConfiguration.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal.config;
+
+import static org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TwoZonesWinecoolerConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class TwoZonesWinecoolerConfiguration {
+ public String apiKey = EMPTY;
+ public String deviceId = EMPTY;
+ public int refreshInterval = MINIMUM_REFRESH_INTERVAL_MIN;
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/CallResponse.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/CallResponse.java
new file mode 100644
index 00000000000..6c5de53f08c
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/CallResponse.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal.dto;
+
+import static org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants.EMPTY;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link CallResponse} class wraps response values of an API call.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class CallResponse {
+ public int status = -1;
+ public String responseString = EMPTY;
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/LightRequest.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/LightRequest.java
new file mode 100644
index 00000000000..f195a4e617f
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/LightRequest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal.dto;
+
+import static org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants.EMPTY;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link LightRequest} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class LightRequest {
+ public String technicalDeviceId = EMPTY;
+ public int zone = -1;
+ public boolean lightOn = false;
+
+ public boolean isValid() {
+ return !technicalDeviceId.equals(EMPTY) && zone >= 0;
+ }
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/StatusRequest.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/StatusRequest.java
new file mode 100644
index 00000000000..f24014d6381
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/StatusRequest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants;
+
+/**
+ * The {@link StatusRequest} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class StatusRequest {
+
+ public StatusRequest(String intialValue) {
+ technicalDeviceId = intialValue;
+ }
+
+ public String technicalDeviceId = CasoKitchenBindingConstants.EMPTY;
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/StatusResult.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/StatusResult.java
new file mode 100644
index 00000000000..b986a26cced
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/dto/StatusResult.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants;
+
+/**
+ * The {@link CasoConfiguration2} class contains fields mapping thing configuration parameters.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class StatusResult {
+ public int temperature1 = -1;
+ public int targetTemperature1 = -1;
+ public boolean power1 = false;
+ public boolean light1 = false;
+
+ public int temperature2 = -1;
+ public int targetTemperature2 = -1;
+ public boolean power2 = false;
+ public boolean light2 = false;
+
+ public String logTimestampUtc = CasoKitchenBindingConstants.EMPTY;
+ public String hint = CasoKitchenBindingConstants.EMPTY;
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/handler/TwoZonesWinecoolerHandler.java b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/handler/TwoZonesWinecoolerHandler.java
new file mode 100644
index 00000000000..6ce369578da
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/java/org/openhab/binding/casokitchen/internal/handler/TwoZonesWinecoolerHandler.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2010-2025 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.casokitchen.internal.handler;
+
+import static org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants.*;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+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.openhab.binding.casokitchen.internal.config.TwoZonesWinecoolerConfiguration;
+import org.openhab.binding.casokitchen.internal.dto.CallResponse;
+import org.openhab.binding.casokitchen.internal.dto.LightRequest;
+import org.openhab.binding.casokitchen.internal.dto.StatusRequest;
+import org.openhab.binding.casokitchen.internal.dto.StatusResult;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+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.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TwoZonesWinecoolerHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class TwoZonesWinecoolerHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(TwoZonesWinecoolerHandler.class);
+
+ private final TimeZoneProvider timeZoneProvider;
+ private final HttpClient httpClient;
+ private Optional> refreshJob = Optional.empty();
+ private Optional cachedResult = Optional.empty();
+ private TwoZonesWinecoolerConfiguration configuration = new TwoZonesWinecoolerConfiguration();
+
+ public TwoZonesWinecoolerHandler(Thing thing, HttpClient hc, TimeZoneProvider tzp) {
+ super(thing);
+ httpClient = hc;
+ timeZoneProvider = tzp;
+ }
+
+ @Override
+ public void initialize() {
+ configuration = getConfigAs(TwoZonesWinecoolerConfiguration.class);
+ String configInvalidReason = configValid();
+ if (configInvalidReason.isEmpty()) {
+ updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE,
+ "@text/casokitchen.winecooler-2z.status.wait-for-response");
+ startSchedule();
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configInvalidReason);
+ }
+ }
+
+ private String configValid() {
+ if (configuration.apiKey.isBlank()) {
+ return "@text/casokitchen.winecooler-2z.status.api-key-missing";
+ } else if (configuration.deviceId.isBlank()) {
+ return "@text/casokitchen.winecooler-2z.status.device-id-missing";
+ } else if (configuration.refreshInterval < MINIMUM_REFRESH_INTERVAL_MIN) {
+ return "@text/casokitchen.winecooler-2z.status.refresh-interval [\"" + configuration.refreshInterval
+ + "\"]";
+ } else {
+ return EMPTY;
+ }
+ }
+
+ private void startSchedule() {
+ refreshJob.ifPresent(job -> {
+ job.cancel(false);
+ });
+ refreshJob = Optional.of(
+ scheduler.scheduleWithFixedDelay(this::dataUpdate, 0, configuration.refreshInterval, TimeUnit.MINUTES));
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String group = channelUID.getGroupId();
+ if (group == null) {
+ return; // no channels without group defined!
+ }
+ if (command instanceof RefreshType) {
+ cachedResult.ifPresent(result -> {
+ // update channels from cached result if available
+ String channel = channelUID.getIdWithoutGroup();
+ switch (group) {
+ case GENERIC:
+ switch (channel) {
+ case LIGHT:
+ updateState(new ChannelUID(thing.getUID(), GENERIC, LIGHT),
+ OnOffType.from(result.light1 && result.light2));
+ break;
+ case LAST_UPDATE:
+ Instant timestamp = Instant.parse(result.logTimestampUtc);
+ updateState(new ChannelUID(thing.getUID(), GENERIC, LAST_UPDATE),
+ new DateTimeType(timestamp.atZone(timeZoneProvider.getTimeZone())));
+ break;
+ case HINT:
+ updateState(new ChannelUID(thing.getUID(), GENERIC, HINT),
+ StringType.valueOf(result.hint));
+ break;
+ }
+ break;
+ case TOP:
+ case BOTTOM:
+ switch (channel) {
+ case LIGHT:
+ updateState(new ChannelUID(thing.getUID(), group, LIGHT),
+ OnOffType.from(result.light2));
+ break;
+ case POWER:
+ updateState(new ChannelUID(thing.getUID(), group, POWER),
+ OnOffType.from(result.power2));
+ break;
+ case TEMPERATURE:
+ updateState(new ChannelUID(thing.getUID(), group, TEMPERATURE),
+ QuantityType.valueOf(result.temperature2, SIUnits.CELSIUS));
+ break;
+ case TARGET_TEMPERATURE:
+ updateState(new ChannelUID(thing.getUID(), group, TARGET_TEMPERATURE),
+ QuantityType.valueOf(result.targetTemperature2, SIUnits.CELSIUS));
+ break;
+ }
+ break;
+ }
+ });
+ } else if (LIGHT.equals(channelUID.getIdWithoutGroup())) {
+ LightRequest lr = new LightRequest();
+ lr.technicalDeviceId = configuration.deviceId;
+ if (command instanceof OnOffType) {
+ lr.lightOn = OnOffType.ON.equals(command);
+ switch (group) {
+ case GENERIC:
+ lr.zone = 0;
+ break;
+ case TOP:
+ lr.zone = 1;
+ break;
+ case BOTTOM:
+ lr.zone = 2;
+ break;
+ }
+ CallResponse cr = post(LIGHT_URL, lr);
+ if (cr.status == 200) {
+ updateState(new ChannelUID(thing.getUID(), group, LIGHT), OnOffType.from(lr.lightOn));
+ } else {
+ logger.warn("Call to {} responded with status {} reason {}", LIGHT_URL, cr.status,
+ cr.responseString);
+ }
+ }
+ logger.debug("Cannot handle command {}", command);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ refreshJob.ifPresent(job -> {
+ job.cancel(true);
+ });
+ }
+
+ private void dataUpdate() {
+ StatusRequest requestContent = new StatusRequest(configuration.deviceId);
+ CallResponse cr = post(STATUS_URL, requestContent);
+ int responseStatus = cr.status;
+ String responseContent = cr.responseString;
+ if (responseStatus == 200) {
+ updateStatus(ThingStatus.ONLINE);
+ StatusResult statusResult = GSON.fromJson(responseContent, StatusResult.class);
+ if (statusResult != null) {
+ updateChannels(statusResult);
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/casokitchen.winecooler-2z.status.http-status [\"" + responseStatus + " - " + responseContent
+ + "\"]");
+ }
+ }
+
+ private void updateChannels(StatusResult result) {
+ cachedResult = Optional.of(result);
+ updateState(new ChannelUID(thing.getUID(), GENERIC, HINT), StringType.valueOf(result.hint));
+ updateState(new ChannelUID(thing.getUID(), GENERIC, LIGHT), OnOffType.from(result.light1 && result.light2));
+ updateState(new ChannelUID(thing.getUID(), TOP, TEMPERATURE),
+ QuantityType.valueOf(result.temperature1, SIUnits.CELSIUS));
+ updateState(new ChannelUID(thing.getUID(), TOP, TARGET_TEMPERATURE),
+ QuantityType.valueOf(result.targetTemperature1, SIUnits.CELSIUS));
+ updateState(new ChannelUID(thing.getUID(), TOP, POWER), OnOffType.from(result.power1));
+ updateState(new ChannelUID(thing.getUID(), TOP, LIGHT), OnOffType.from(result.light1));
+ updateState(new ChannelUID(thing.getUID(), BOTTOM, TEMPERATURE),
+ QuantityType.valueOf(result.temperature2, SIUnits.CELSIUS));
+ updateState(new ChannelUID(thing.getUID(), BOTTOM, TARGET_TEMPERATURE),
+ QuantityType.valueOf(result.targetTemperature2, SIUnits.CELSIUS));
+ updateState(new ChannelUID(thing.getUID(), BOTTOM, POWER), OnOffType.from(result.power2));
+ updateState(new ChannelUID(thing.getUID(), BOTTOM, LIGHT), OnOffType.from(result.light2));
+
+ ZonedDateTime zdt = Instant.parse(result.logTimestampUtc).atZone(timeZoneProvider.getTimeZone());
+ updateState(new ChannelUID(thing.getUID(), GENERIC, LAST_UPDATE), new DateTimeType(zdt));
+ }
+
+ private CallResponse post(String url, Object dto) {
+ Request req = httpClient.POST(url);
+ req.header(HttpHeader.CONTENT_TYPE, "application/json");
+ req.header(HTTP_HEADER_API_KEY, configuration.apiKey);
+ req.content(new StringContentProvider(GSON.toJson(dto)));
+ CallResponse callResponse = new CallResponse();
+ try {
+ ContentResponse cr = req.timeout(60, TimeUnit.SECONDS).send();
+ callResponse.status = cr.getStatus();
+ callResponse.responseString = cr.getContentAsString();
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ String message = e.getMessage();
+ callResponse.responseString = ((message != null) ? message : EMPTY);
+ }
+ return callResponse;
+ }
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 00000000000..392baf398f1
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,11 @@
+
+
+
+ binding
+ CASO Kitchen Binding
+ Binding to connect CASO Smart Kitchen devices
+ cloud
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/i18n/casokitchen.properties b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/i18n/casokitchen.properties
new file mode 100644
index 00000000000..99257e3e72b
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/i18n/casokitchen.properties
@@ -0,0 +1,55 @@
+# add-on
+
+addon.casokitchen.name = CASO Kitchen Binding
+addon.casokitchen.description = Binding to connect CASO Smart Kitchen devices
+
+# thing types
+
+thing-type.casokitchen.winecooler-2z.label = Wine Cooler 2 Zones
+thing-type.casokitchen.winecooler-2z.description = Wine cooler with 2 cooling zones
+
+# thing types config
+
+thing-type.config.casokitchen.winecooler-2z.apiKey.label = API Key
+thing-type.config.casokitchen.winecooler-2z.apiKey.description = API Key generated via CASO SMart Kitchen API
+thing-type.config.casokitchen.winecooler-2z.deviceId.label = Device ID
+thing-type.config.casokitchen.winecooler-2z.deviceId.description = Device ID from CASO connected devices
+thing-type.config.casokitchen.winecooler-2z.refreshInterval.label = Refresh Interval
+thing-type.config.casokitchen.winecooler-2z.refreshInterval.description = Interval the device is polled in minutes.
+
+# channel group types
+
+channel-group-type.casokitchen.bottom-values.label = Bottom Zone
+channel-group-type.casokitchen.bottom-values.channel.power.label = Zone is Powered
+channel-group-type.casokitchen.bottom-values.channel.power.description = Showing if zone is currently powered
+channel-group-type.casokitchen.generic-values.label = Generic Values
+channel-group-type.casokitchen.top-values.label = Top Zone
+channel-group-type.casokitchen.top-values.channel.power.label = Zone is Powered
+channel-group-type.casokitchen.top-values.channel.power.description = Showing if zone is currently powered
+
+# channel types
+
+channel-type.casokitchen.hint.label = Hint
+channel-type.casokitchen.hint.description = Textual hint for device status
+channel-type.casokitchen.last-update.label = Last Update
+channel-type.casokitchen.last-update.description = Time stamp of latest device communication
+channel-type.casokitchen.last-update.state.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
+channel-type.casokitchen.light-switch.label = Light Switch
+channel-type.casokitchen.light-switch.description = Switching lights on and off
+channel-type.casokitchen.set-temperature.label = Target Temperature
+channel-type.casokitchen.set-temperature.description = Target Zone Temperature
+channel-type.casokitchen.temperature.label = Temperature
+channel-type.casokitchen.temperature.description = Current Zone Temperature
+
+# channel types
+
+channel-type.casokitchen.power.label = Zone is Powered
+channel-type.casokitchen.power.description = Showing if zone is currently powered
+
+# status details
+
+casokitchen.winecooler-2z.status.api-key-missing = API Key is mandatory
+casokitchen.winecooler-2z.status.device-id-missing = Device ID is mandatory
+casokitchen.winecooler-2z.status.refresh-interval = Refresh interval {0} not supported
+casokitchen.winecooler-2z.status.http-status = HTTP Status Code {0}
+casokitchen.winecooler-2z.status.wait-for-response = Wait for response
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/bottom-zone-group.xml b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/bottom-zone-group.xml
new file mode 100644
index 00000000000..bb58e777669
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/bottom-zone-group.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ Showing if zone is currently powered
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/channel-types.xml
new file mode 100644
index 00000000000..0b612103b86
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/channel-types.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ Number:Temperature
+
+ Current Zone Temperature
+
+ Temperature
+ Measurement
+
+
+
+
+ Number:Temperature
+
+ Target Zone Temperature
+
+ Temperature
+ SetPoint
+
+
+
+
+ Switch
+
+ Switching lights on and off
+ veto
+
+
+ String
+
+ Textual hint for device status
+
+
+
+ DateTime
+
+ Time stamp of latest device communication
+
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/generic-group.xml b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/generic-group.xml
new file mode 100644
index 00000000000..13e5984a7e0
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/generic-group.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..f2f85877ca6
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+ Wine cooler with 2 cooling zones
+
+
+
+
+
+
+
+
+
+
+ API Key generated via CASO SMart Kitchen API
+
+
+
+ Device ID from CASO connected devices
+
+
+
+ Interval the device is polled in minutes.
+ 5
+
+
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/top-zone-group.xml b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/top-zone-group.xml
new file mode 100644
index 00000000000..122a6cc0040
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/main/resources/OH-INF/thing/top-zone-group.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+ Showing if zone is currently powered
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/CallbackMock.java b/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/CallbackMock.java
new file mode 100644
index 00000000000..04ab6b23d4d
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/CallbackMock.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2010-2025 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.caso.internal;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.config.core.ConfigDescription;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelGroupUID;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.type.ChannelGroupTypeUID;
+import org.openhab.core.thing.type.ChannelTypeUID;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.TimeSeries;
+
+/**
+ * {@link CallbackMock} listener for handler updates
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class CallbackMock implements ThingHandlerCallback {
+ public Map states = new HashMap<>();
+ public ThingStatus thingStatus = ThingStatus.UNINITIALIZED;
+
+ @Override
+ public void stateUpdated(ChannelUID channelUID, State state) {
+ states.put(channelUID.toString(), state);
+ }
+
+ public void waitForFullUpdate(int stateCount) {
+ Instant startWaiting = Instant.now();
+ while (states.size() < stateCount && startWaiting.plus(5, ChronoUnit.SECONDS).isAfter(Instant.now())) {
+ try {
+ Thread.sleep(250);
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ }
+ if (!ThingStatus.ONLINE.equals(thingStatus)) {
+ fail(thingStatus.toString());
+ }
+ }
+
+ @Override
+ public void postCommand(ChannelUID channelUID, Command command) {
+ }
+
+ @Override
+ public void sendTimeSeries(ChannelUID channelUID, TimeSeries timeSeries) {
+ }
+
+ public void waitForOnline() {
+ synchronized (this) {
+ Instant startWaiting = Instant.now();
+ while (!ThingStatus.ONLINE.equals(thingStatus)
+ && startWaiting.plus(5, ChronoUnit.SECONDS).isAfter(Instant.now())) {
+ try {
+ wait(250);
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ }
+ if (!ThingStatus.ONLINE.equals(thingStatus)) {
+ fail(thingStatus.toString());
+ }
+ }
+ }
+
+ @Override
+ public void statusUpdated(Thing thing, ThingStatusInfo thingStatusInfo) {
+ synchronized (this) {
+ thing.setStatusInfo(thingStatusInfo);
+ thingStatus = thingStatusInfo.getStatus();
+ notifyAll();
+ }
+ }
+
+ @Override
+ public void thingUpdated(Thing thing) {
+ }
+
+ @Override
+ public void validateConfigurationParameters(Thing thing, Map configurationParameters) {
+ }
+
+ @Override
+ public void validateConfigurationParameters(Channel channel, Map configurationParameters) {
+ }
+
+ @Override
+ public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) {
+ return null;
+ }
+
+ @Override
+ public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) {
+ return null;
+ }
+
+ @Override
+ public void configurationUpdated(Thing thing) {
+ }
+
+ @Override
+ public void migrateThingType(Thing thing, ThingTypeUID thingTypeUID, Configuration configuration) {
+ }
+
+ @Override
+ public void channelTriggered(Thing thing, ChannelUID channelUID, String event) {
+ }
+
+ @Override
+ public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) {
+ return ChannelBuilder.create(new ChannelUID("test"), null);
+ }
+
+ @Override
+ public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) {
+ return ChannelBuilder.create(new ChannelUID("test"), null);
+ }
+
+ @Override
+ public List createChannelBuilders(ChannelGroupUID channelGroupUID,
+ ChannelGroupTypeUID channelGroupTypeUID) {
+ return List.of();
+ }
+
+ @Override
+ public boolean isChannelLinked(ChannelUID channelUID) {
+ return false;
+ }
+
+ @Override
+ public @Nullable Bridge getBridge(ThingUID bridgeUID) {
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/FactoryMock.java b/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/FactoryMock.java
new file mode 100644
index 00000000000..80a58a80cdf
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/FactoryMock.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2010-2025 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.caso.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.casokitchen.internal.CasoKitchenHandlerFactory;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * {@link FactoryMock} for creating unit test handlers
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+public class FactoryMock extends CasoKitchenHandlerFactory {
+
+ public FactoryMock(HttpClientFactory httpFactory, final TimeZoneProvider tzp) {
+ super(httpFactory, tzp);
+ }
+
+ @Override
+ public @Nullable ThingHandler createHandler(Thing thing) {
+ return super.createHandler(thing);
+ }
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/TestHandler.java b/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/TestHandler.java
new file mode 100644
index 00000000000..7106383a82d
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/test/java/org/openhab/binding/caso/internal/TestHandler.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2010-2025 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.caso.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.casokitchen.internal.CasoKitchenBindingConstants;
+import org.openhab.binding.casokitchen.internal.handler.TwoZonesWinecoolerHandler;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.internal.ThingImpl;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link TestHandler} is testing handler functions
+ *
+ * @author Bernd Weymann - Initial contribution
+ */
+@NonNullByDefault
+class TestHandler {
+ private static final int FULL_UPDATE_COUNT = 11;
+
+ TimeZoneProvider tzp = new TimeZoneProvider() {
+ @Override
+ public ZoneId getTimeZone() {
+ return ZoneId.systemDefault();
+ }
+ };
+
+ private HttpClientFactory prepareHttpResponse() {
+ // Prepare http response
+ HttpClientFactory httpFactory = mock(HttpClientFactory.class);
+ HttpClient httpClient = mock(HttpClient.class);
+ when(httpFactory.getCommonHttpClient()).thenReturn(httpClient);
+ Request httpStatusRequest = mock(Request.class);
+ when(httpClient.POST(CasoKitchenBindingConstants.STATUS_URL)).thenReturn(httpStatusRequest);
+ ContentResponse contentResponse = mock(ContentResponse.class);
+ when(contentResponse.getStatus()).thenReturn(200);
+ String content = CasoKitchenBindingConstants.EMPTY;
+ try {
+ content = Files.readString(Path.of("src/test/resources/", "StatusResponse.json"), Charset.defaultCharset());
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ when(contentResponse.getContentAsString()).thenReturn(content);
+ when(httpStatusRequest.timeout(anyLong(), any(TimeUnit.class))).thenReturn(httpStatusRequest);
+ try {
+ when(httpStatusRequest.send()).thenReturn(contentResponse);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ fail(e.getMessage());
+ }
+ return httpFactory;
+ }
+
+ @Test
+ void testConfigErrors() {
+ ThingImpl thing = new ThingImpl(CasoKitchenBindingConstants.THING_TYPE_WINECOOLER, "test");
+
+ FactoryMock factory = new FactoryMock(prepareHttpResponse(), tzp);
+ ThingHandler handler = factory.createHandler(thing);
+ assertNotNull(handler);
+ assertTrue(handler instanceof TwoZonesWinecoolerHandler);
+ TwoZonesWinecoolerHandler winecoolerHandler = (TwoZonesWinecoolerHandler) handler;
+ CallbackMock callback = new CallbackMock();
+ winecoolerHandler.setCallback(callback);
+ winecoolerHandler.initialize();
+
+ ThingStatusInfo tsi = thing.getStatusInfo();
+ assertEquals(ThingStatus.OFFLINE, tsi.getStatus());
+ assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, tsi.getStatusDetail());
+ assertEquals("@text/casokitchen.winecooler-2z.status.api-key-missing", tsi.getDescription());
+
+ Configuration config = new Configuration();
+ config.put("apiKey", "abc");
+ thing.setConfiguration(config);
+ winecoolerHandler.initialize();
+ tsi = thing.getStatusInfo();
+ assertEquals(ThingStatus.OFFLINE, tsi.getStatus());
+ assertEquals(ThingStatusDetail.CONFIGURATION_ERROR, tsi.getStatusDetail());
+ assertEquals("@text/casokitchen.winecooler-2z.status.device-id-missing", tsi.getDescription());
+
+ config.put("deviceId", "xyz");
+ thing.setConfiguration(config);
+ winecoolerHandler.initialize();
+ tsi = thing.getStatusInfo();
+ assertEquals(ThingStatus.UNKNOWN, tsi.getStatus());
+ assertEquals(ThingStatusDetail.NONE, tsi.getStatusDetail());
+ assertEquals("@text/casokitchen.winecooler-2z.status.wait-for-response", tsi.getDescription());
+ }
+
+ @Test
+ void testHandler() {
+ // Prepare Thing
+ ThingImpl thing = new ThingImpl(CasoKitchenBindingConstants.THING_TYPE_WINECOOLER, "test");
+ Configuration config = new Configuration();
+ config.put("apiKey", "abc");
+ config.put("deviceId", "xyz");
+ thing.setConfiguration(config);
+
+ // Prepare handler
+ FactoryMock factory = new FactoryMock(prepareHttpResponse(), tzp);
+ ThingHandler handler = factory.createHandler(thing);
+ assertNotNull(handler);
+ assertTrue(handler instanceof TwoZonesWinecoolerHandler);
+ TwoZonesWinecoolerHandler winecoolerHandler = (TwoZonesWinecoolerHandler) handler;
+ CallbackMock callback = new CallbackMock();
+ winecoolerHandler.setCallback(callback);
+ winecoolerHandler.initialize();
+ callback.waitForOnline();
+ callback.waitForFullUpdate(FULL_UPDATE_COUNT);
+
+ // generic
+ assertEquals(OnOffType.OFF, callback.states.get("casokitchen:winecooler-2z:test:generic#light-switch"));
+ State dateTime = callback.states.get("casokitchen:winecooler-2z:test:generic#last-update");
+ assertTrue(dateTime instanceof DateTimeType);
+ Instant lastTimestamp = ((DateTimeType) dateTime).getInstant();
+ assertEquals("2024-08-13T23:25:32.238209200Z", lastTimestamp.toString());
+
+ // top
+ State currentTopTemp = callback.states.get("casokitchen:winecooler-2z:test:top#temperature");
+ assertTrue(currentTopTemp instanceof QuantityType);
+ assertEquals("9 °C", currentTopTemp.toFullString());
+ State currentTopSetTemp = callback.states.get("casokitchen:winecooler-2z:test:top#set-temperature");
+ assertTrue(currentTopSetTemp instanceof QuantityType);
+ assertEquals("9 °C", currentTopSetTemp.toFullString());
+ assertEquals(OnOffType.ON, callback.states.get("casokitchen:winecooler-2z:test:top#power"));
+ assertEquals(OnOffType.OFF, callback.states.get("casokitchen:winecooler-2z:test:top#light-switch"));
+
+ // bottom
+ State currentBottomTemp = callback.states.get("casokitchen:winecooler-2z:test:bottom#temperature");
+ assertTrue(currentBottomTemp instanceof QuantityType);
+ assertEquals("10 °C", currentBottomTemp.toFullString());
+ State currentBottomSetTemp = callback.states.get("casokitchen:winecooler-2z:test:bottom#set-temperature");
+ assertTrue(currentBottomSetTemp instanceof QuantityType);
+ assertEquals("10 °C", currentBottomSetTemp.toFullString());
+ assertEquals(OnOffType.ON, callback.states.get("casokitchen:winecooler-2z:test:bottom#power"));
+ assertEquals(OnOffType.OFF, callback.states.get("casokitchen:winecooler-2z:test:bottom#light-switch"));
+ }
+}
diff --git a/bundles/org.openhab.binding.casokitchen/src/test/resources/StatusResponse.json b/bundles/org.openhab.binding.casokitchen/src/test/resources/StatusResponse.json
new file mode 100644
index 00000000000..5488dc1b477
--- /dev/null
+++ b/bundles/org.openhab.binding.casokitchen/src/test/resources/StatusResponse.json
@@ -0,0 +1,13 @@
+{
+ "temperature1": 9,
+ "targetTemperature1": 9,
+ "temperature2": 10,
+ "targetTemperature2": 10,
+ "power1": true,
+ "power2": true,
+ "light1": false,
+ "light2": false,
+ "logTimestampUtc": "2024-08-13T23:25:32.2382092Z",
+ "temperatureUnit": "C",
+ "hint": "Note: Device status and content of this message may differ due to synchronization intervals of distributed systems."
+}
\ No newline at end of file
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 9a4125eff6b..2aa1a17fb2f 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -98,6 +98,7 @@
org.openhab.binding.bticinosmarther
org.openhab.binding.buienradar
org.openhab.binding.caddx
+ org.openhab.binding.casokitchen
org.openhab.binding.cbus
org.openhab.binding.chatgpt
org.openhab.binding.chromecast