[casokitchen] Initial contribution (#18243)
* initial commit Signed-off-by: Bernd Weymann <bernd.weymann@gmail.com>pull/18343/head
parent
9ff0eba7e7
commit
efc103c464
|
@ -65,6 +65,7 @@
|
||||||
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
|
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
|
||||||
/bundles/org.openhab.binding.buienradar/ @gedejong
|
/bundles/org.openhab.binding.buienradar/ @gedejong
|
||||||
/bundles/org.openhab.binding.caddx/ @jossuar
|
/bundles/org.openhab.binding.caddx/ @jossuar
|
||||||
|
/bundles/org.openhab.binding.casokitchen/ @weymann
|
||||||
/bundles/org.openhab.binding.cbus/ @jpharvey
|
/bundles/org.openhab.binding.cbus/ @jpharvey
|
||||||
/bundles/org.openhab.binding.chatgpt/ @kaikreuzer
|
/bundles/org.openhab.binding.chatgpt/ @kaikreuzer
|
||||||
/bundles/org.openhab.binding.chromecast/ @kaikreuzer
|
/bundles/org.openhab.binding.chromecast/ @kaikreuzer
|
||||||
|
|
|
@ -311,6 +311,11 @@
|
||||||
<artifactId>org.openhab.binding.caddx</artifactId>
|
<artifactId>org.openhab.binding.caddx</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.casokitchen</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.cbus</artifactId>
|
<artifactId>org.openhab.binding.cbus</artifactId>
|
||||||
|
|
|
@ -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
|
|
@ -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" }
|
||||||
|
```
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||||
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>org.openhab.binding.casokitchen</artifactId>
|
||||||
|
|
||||||
|
<name>openHAB Add-ons :: Bundles :: CasoKitchen Binding</name>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<features name="org.openhab.binding.casokitchen-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||||
|
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||||
|
|
||||||
|
<feature name="openhab-binding-casokitchen" description="CasoKitchen Binding" version="${project.version}">
|
||||||
|
<feature>openhab-runtime-base</feature>
|
||||||
|
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.casokitchen/${project.version}</bundle>
|
||||||
|
</feature>
|
||||||
|
</features>
|
|
@ -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();
|
||||||
|
}
|
|
@ -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<ThingTypeUID> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<ScheduledFuture<?>> refreshJob = Optional.empty();
|
||||||
|
private Optional<StatusResult> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<addon:addon id="casokitchen" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||||
|
|
||||||
|
<type>binding</type>
|
||||||
|
<name>CASO Kitchen Binding</name>
|
||||||
|
<description>Binding to connect CASO Smart Kitchen devices</description>
|
||||||
|
<connection>cloud</connection>
|
||||||
|
|
||||||
|
</addon:addon>
|
|
@ -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
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="casokitchen"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||||
|
<channel-group-type id="bottom-values">
|
||||||
|
<label>Bottom Zone</label>
|
||||||
|
<channels>
|
||||||
|
<channel id="power" typeId="system.power">
|
||||||
|
<label>Zone is Powered</label>
|
||||||
|
<description>Showing if zone is currently powered</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="temperature" typeId="temperature"/>
|
||||||
|
<channel id="set-temperature" typeId="set-temperature"/>
|
||||||
|
<channel id="light-switch" typeId="light-switch"/>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
</thing:thing-descriptions>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="casokitchen"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||||
|
|
||||||
|
<channel-type id="temperature">
|
||||||
|
<item-type>Number:Temperature</item-type>
|
||||||
|
<label>Temperature</label>
|
||||||
|
<description>Current Zone Temperature</description>
|
||||||
|
<tags>
|
||||||
|
<tag>Temperature</tag>
|
||||||
|
<tag>Measurement</tag>
|
||||||
|
</tags>
|
||||||
|
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="set-temperature">
|
||||||
|
<item-type>Number:Temperature</item-type>
|
||||||
|
<label>Target Temperature</label>
|
||||||
|
<description>Target Zone Temperature</description>
|
||||||
|
<tags>
|
||||||
|
<tag>Temperature</tag>
|
||||||
|
<tag>SetPoint</tag>
|
||||||
|
</tags>
|
||||||
|
<state pattern="%.1f %unit%"/>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="light-switch">
|
||||||
|
<item-type>Switch</item-type>
|
||||||
|
<label>Light Switch</label>
|
||||||
|
<description>Switching lights on and off</description>
|
||||||
|
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="hint">
|
||||||
|
<item-type>String</item-type>
|
||||||
|
<label>Hint</label>
|
||||||
|
<description>Textual hint for device status</description>
|
||||||
|
<state readOnly="true"/>
|
||||||
|
</channel-type>
|
||||||
|
<channel-type id="last-update">
|
||||||
|
<item-type>DateTime</item-type>
|
||||||
|
<label>Last Update</label>
|
||||||
|
<description>Time stamp of latest device communication</description>
|
||||||
|
<state pattern="%1$tA, %1$td.%1$tm. %1$tH:%1$tM" readOnly="true"/>
|
||||||
|
</channel-type>
|
||||||
|
</thing:thing-descriptions>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="casokitchen"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||||
|
<channel-group-type id="generic-values">
|
||||||
|
<label>Generic Values</label>
|
||||||
|
<channels>
|
||||||
|
<channel id="light-switch" typeId="light-switch"/>
|
||||||
|
<channel id="hint" typeId="hint"/>
|
||||||
|
<channel id="last-update" typeId="last-update"/>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
</thing:thing-descriptions>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="casokitchen"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||||
|
|
||||||
|
<thing-type id="winecooler-2z">
|
||||||
|
<label>Wine Cooler 2 Zones</label>
|
||||||
|
<description>Wine cooler with 2 cooling zones</description>
|
||||||
|
|
||||||
|
<channel-groups>
|
||||||
|
<channel-group id="top" typeId="top-values"/>
|
||||||
|
<channel-group id="bottom" typeId="bottom-values"/>
|
||||||
|
<channel-group id="generic" typeId="generic-values"/>
|
||||||
|
</channel-groups>
|
||||||
|
|
||||||
|
<config-description>
|
||||||
|
<parameter name="apiKey" type="text" required="true">
|
||||||
|
<label>API Key</label>
|
||||||
|
<description>API Key generated via CASO SMart Kitchen API</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="deviceId" type="text" required="true">
|
||||||
|
<label>Device ID</label>
|
||||||
|
<description>Device ID from CASO connected devices</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="refreshInterval" type="integer" unit="m" min="5">
|
||||||
|
<label>Refresh Interval</label>
|
||||||
|
<description>Interval the device is polled in minutes.</description>
|
||||||
|
<default>5</default>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</thing-type>
|
||||||
|
</thing:thing-descriptions>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<thing:thing-descriptions bindingId="casokitchen"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||||
|
<channel-group-type id="top-values">
|
||||||
|
<label>Top Zone</label>
|
||||||
|
<channels>
|
||||||
|
<channel id="power" typeId="system.power">
|
||||||
|
<label>Zone is Powered</label>
|
||||||
|
<description>Showing if zone is currently powered</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="temperature" typeId="temperature"/>
|
||||||
|
<channel id="set-temperature" typeId="set-temperature"/>
|
||||||
|
<channel id="light-switch" typeId="light-switch"/>
|
||||||
|
</channels>
|
||||||
|
</channel-group-type>
|
||||||
|
</thing:thing-descriptions>
|
|
@ -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<String, State> 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<String, Object> configurationParameters) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateConfigurationParameters(Channel channel, Map<String, Object> 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<ChannelBuilder> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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."
|
||||||
|
}
|
|
@ -98,6 +98,7 @@
|
||||||
<module>org.openhab.binding.bticinosmarther</module>
|
<module>org.openhab.binding.bticinosmarther</module>
|
||||||
<module>org.openhab.binding.buienradar</module>
|
<module>org.openhab.binding.buienradar</module>
|
||||||
<module>org.openhab.binding.caddx</module>
|
<module>org.openhab.binding.caddx</module>
|
||||||
|
<module>org.openhab.binding.casokitchen</module>
|
||||||
<module>org.openhab.binding.cbus</module>
|
<module>org.openhab.binding.cbus</module>
|
||||||
<module>org.openhab.binding.chatgpt</module>
|
<module>org.openhab.binding.chatgpt</module>
|
||||||
<module>org.openhab.binding.chromecast</module>
|
<module>org.openhab.binding.chromecast</module>
|
||||||
|
|
Loading…
Reference in New Issue