[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.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
|
||||
|
|
|
@ -311,6 +311,11 @@
|
|||
<artifactId>org.openhab.binding.caddx</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.casokitchen</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<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.buienradar</module>
|
||||
<module>org.openhab.binding.caddx</module>
|
||||
<module>org.openhab.binding.casokitchen</module>
|
||||
<module>org.openhab.binding.cbus</module>
|
||||
<module>org.openhab.binding.chatgpt</module>
|
||||
<module>org.openhab.binding.chromecast</module>
|
||||
|
|
Loading…
Reference in New Issue