added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.deconz-${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-deconz" description="Dresden Elektronik deCONZ Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-http</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.deconz/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,123 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class BindingConstants {
public static final String BINDING_ID = "deconz";
// List of all Thing Type UIDs
public static final ThingTypeUID BRIDGE_TYPE = new ThingTypeUID(BINDING_ID, "deconz");
// sensors
public static final ThingTypeUID THING_TYPE_PRESENCE_SENSOR = new ThingTypeUID(BINDING_ID, "presencesensor");
public static final ThingTypeUID THING_TYPE_POWER_SENSOR = new ThingTypeUID(BINDING_ID, "powersensor");
public static final ThingTypeUID THING_TYPE_CONSUMPTION_SENSOR = new ThingTypeUID(BINDING_ID, "consumptionsensor");
public static final ThingTypeUID THING_TYPE_DAYLIGHT_SENSOR = new ThingTypeUID(BINDING_ID, "daylightsensor");
public static final ThingTypeUID THING_TYPE_COLOR_CONTROL = new ThingTypeUID(BINDING_ID, "colorcontrol");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final ThingTypeUID THING_TYPE_LIGHT_SENSOR = new ThingTypeUID(BINDING_ID, "lightsensor");
public static final ThingTypeUID THING_TYPE_TEMPERATURE_SENSOR = new ThingTypeUID(BINDING_ID, "temperaturesensor");
public static final ThingTypeUID THING_TYPE_HUMIDITY_SENSOR = new ThingTypeUID(BINDING_ID, "humiditysensor");
public static final ThingTypeUID THING_TYPE_PRESSURE_SENSOR = new ThingTypeUID(BINDING_ID, "pressuresensor");
public static final ThingTypeUID THING_TYPE_OPENCLOSE_SENSOR = new ThingTypeUID(BINDING_ID, "openclosesensor");
public static final ThingTypeUID THING_TYPE_WATERLEAKAGE_SENSOR = new ThingTypeUID(BINDING_ID,
"waterleakagesensor");
public static final ThingTypeUID THING_TYPE_FIRE_SENSOR = new ThingTypeUID(BINDING_ID, "firesensor");
public static final ThingTypeUID THING_TYPE_ALARM_SENSOR = new ThingTypeUID(BINDING_ID, "alarmsensor");
public static final ThingTypeUID THING_TYPE_VIBRATION_SENSOR = new ThingTypeUID(BINDING_ID, "vibrationsensor");
public static final ThingTypeUID THING_TYPE_BATTERY_SENSOR = new ThingTypeUID(BINDING_ID, "batterysensor");
public static final ThingTypeUID THING_TYPE_CARBONMONOXIDE_SENSOR = new ThingTypeUID(BINDING_ID,
"carbonmonoxidesensor");
// Special sensor - Thermostat
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
// lights
public static final ThingTypeUID THING_TYPE_ONOFF_LIGHT = new ThingTypeUID(BINDING_ID, "onofflight");
public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmablelight");
public static final ThingTypeUID THING_TYPE_COLOR_TEMPERATURE_LIGHT = new ThingTypeUID(BINDING_ID,
"colortemperaturelight");
public static final ThingTypeUID THING_TYPE_COLOR_LIGHT = new ThingTypeUID(BINDING_ID, "colorlight");
public static final ThingTypeUID THING_TYPE_EXTENDED_COLOR_LIGHT = new ThingTypeUID(BINDING_ID,
"extendedcolorlight");
public static final ThingTypeUID THING_TYPE_WINDOW_COVERING = new ThingTypeUID(BINDING_ID, "windowcovering");
public static final ThingTypeUID THING_TYPE_WARNING_DEVICE = new ThingTypeUID(BINDING_ID, "warningdevice");
// List of all Channel ids
public static final String CHANNEL_PRESENCE = "presence";
public static final String CHANNEL_LAST_UPDATED = "last_updated";
public static final String CHANNEL_LAST_SEEN = "last_seen";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_CONSUMPTION = "consumption";
public static final String CHANNEL_VOLTAGE = "voltage";
public static final String CHANNEL_CURRENT = "current";
public static final String CHANNEL_VALUE = "value";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_PRESSURE = "pressure";
public static final String CHANNEL_LIGHT = "light";
public static final String CHANNEL_LIGHT_LUX = "lightlux";
public static final String CHANNEL_LIGHT_LEVEL = "light_level";
public static final String CHANNEL_DARK = "dark";
public static final String CHANNEL_DAYLIGHT = "daylight";
public static final String CHANNEL_BUTTON = "button";
public static final String CHANNEL_BUTTONEVENT = "buttonevent";
public static final String CHANNEL_GESTURE = "gesture";
public static final String CHANNEL_GESTUREEVENT = "gestureevent";
public static final String CHANNEL_OPENCLOSE = "open";
public static final String CHANNEL_WATERLEAKAGE = "waterleakage";
public static final String CHANNEL_FIRE = "fire";
public static final String CHANNEL_ALARM = "alarm";
public static final String CHANNEL_TAMPERED = "tampered";
public static final String CHANNEL_VIBRATION = "vibration";
public static final String CHANNEL_BATTERY_LEVEL = "battery_level";
public static final String CHANNEL_BATTERY_LOW = "battery_low";
public static final String CHANNEL_CARBONMONOXIDE = "carbonmonoxide";
public static final String CHANNEL_HEATSETPOINT = "heatsetpoint";
public static final String CHANNEL_THERMOSTAT_MODE = "mode";
public static final String CHANNEL_TEMPERATURE_OFFSET = "offset";
public static final String CHANNEL_VALVE_POSITION = "valve";
public static final String CHANNEL_SWITCH = "switch";
public static final String CHANNEL_BRIGHTNESS = "brightness";
public static final String CHANNEL_COLOR_TEMPERATURE = "color_temperature";
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_POSITION = "position";
public static final String CHANNEL_ALERT = "alert";
// Thing configuration
public static final String CONFIG_HOST = "host";
public static final String CONFIG_HTTP_PORT = "httpPort";
public static final String CONFIG_APIKEY = "apikey";
public static final String UNIQUE_ID = "uid";
public static final String PROPERTY_CT_MIN = "ctmin";
public static final String PROPERTY_CT_MAX = "ctmax";
// CT value range according to ZCL Spec
public static final int ZCL_CT_UNDEFINED = 0; // 0x0000
public static final int ZCL_CT_MIN = 1;
public static final int ZCL_CT_MAX = 65279; // 0xFEFF
public static final int ZCL_CT_INVALID = 65535; // 0xFFFF
}

View File

@@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge;
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;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link DeconzHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author David Graeff - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.deconz")
@NonNullByDefault
public class DeconzHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(DeconzBridgeHandler.SUPPORTED_THING_TYPES, LightThingHandler.SUPPORTED_THING_TYPE_UIDS,
SensorThingHandler.SUPPORTED_THING_TYPES, SensorThermostatThingHandler.SUPPORTED_THING_TYPES)
.flatMap(Set::stream).collect(Collectors.toSet());
private final Gson gson;
private final WebSocketFactory webSocketFactory;
private final HttpClientFactory httpClientFactory;
private final StateDescriptionProvider stateDescriptionProvider;
@Activate
public DeconzHandlerFactory(final @Reference WebSocketFactory webSocketFactory,
final @Reference HttpClientFactory httpClientFactory,
final @Reference StateDescriptionProvider stateDescriptionProvider) {
this.webSocketFactory = webSocketFactory;
this.httpClientFactory = httpClientFactory;
this.stateDescriptionProvider = stateDescriptionProvider;
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
@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 (DeconzBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new DeconzBridgeHandler((Bridge) thing, webSocketFactory,
new AsyncHttpClient(httpClientFactory.getCommonHttpClient()), gson);
} else if (LightThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) {
return new LightThingHandler(thing, gson, stateDescriptionProvider);
} else if (SensorThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SensorThingHandler(thing, gson);
} else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SensorThermostatThingHandler(thing, gson);
}
return null;
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dynamic channel state description provider.
* Overrides the state description for the controls, which receive its configuration in the runtime.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, StateDescriptionProvider.class }, immediate = true)
public class StateDescriptionProvider implements DynamicStateDescriptionProvider {
private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(StateDescriptionProvider.class);
/**
* Set a state description for a channel. This description will be used when preparing the channel state by
* the framework for presentation. A previous description, if existed, will be replaced.
*
* @param channelUID
* channel UID
* @param description
* state description for the channel
*/
public void setDescription(ChannelUID channelUID, StateDescription description) {
logger.trace("adding state description for channel {}", channelUID);
descriptions.put(channelUID, description);
}
/**
* remove all descriptions for a given thing
*
* @param thingUID the thing's UID
*/
public void removeDescriptionsForThing(ThingUID thingUID) {
logger.trace("removing state description for thing {}", thingUID);
descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
}
@Override
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
if (descriptions.containsKey(channel.getUID())) {
logger.trace("returning new stateDescription for {}", channel.getUID());
return descriptions.get(channel.getUID());
} else {
return null;
}
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DateTimeType;
/**
* The {@link Util} class defines common utility methods
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class Util {
public static String buildUrl(String host, int port, String... urlParts) {
StringBuilder url = new StringBuilder();
url.append("http://");
url.append(host).append(":").append(port);
url.append("/api/");
if (urlParts.length > 0) {
url.append(Stream.of(urlParts).filter(s -> s != null && !s.isEmpty()).collect(Collectors.joining("/")));
}
return url.toString();
}
public static int miredToKelvin(int miredValue) {
return (int) (1000000.0 / miredValue);
}
public static int kelvinToMired(int kelvinValue) {
return (int) (1000000.0 / kelvinValue);
}
public static int constrainToRange(int intValue, int min, int max) {
return Math.max(min, Math.min(intValue, max));
}
/**
* convert a timestamp string to a DateTimeType
*
* @param timestamp either in zoned date time or local date time format
* @return the corresponding DateTimeType
*/
public static DateTimeType convertTimestampToDateTime(String timestamp) {
if (timestamp.endsWith("Z")) {
return new DateTimeType(ZonedDateTime.parse(timestamp));
} else {
return new DateTimeType(
ZonedDateTime.ofInstant(LocalDateTime.parse(timestamp, DateTimeFormatter.ISO_LOCAL_DATE_TIME),
ZoneOffset.UTC, ZoneId.systemDefault()));
}
}
}

View File

@@ -0,0 +1,90 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.discovery;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.jupnp.model.meta.DeviceDetails;
import org.jupnp.model.meta.RemoteDevice;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
/**
* Discover deCONZ software instances. They announce themselves as HUE bridges,
* and their REST API is compatible to HUE bridges. But they also provide a websocket
* real-time channel for sensors.
*
* We check for the manufacturer string of "dresden elektronik".
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
@Component(service = UpnpDiscoveryParticipant.class, immediate = true)
public class BridgeDiscoveryParticipant implements UpnpDiscoveryParticipant {
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(BRIDGE_TYPE);
}
@Override
public @Nullable DiscoveryResult createResult(RemoteDevice device) {
ThingUID uid = getThingUID(device);
if (uid == null) {
return null;
}
URL descriptorURL = device.getIdentity().getDescriptorURL();
String UDN = device.getIdentity().getUdn().getIdentifierString();
// Friendly name is like "name (host)"
String name = device.getDetails().getFriendlyName();
// Cut out the pure name
if (name.indexOf('(') - 1 > 0) {
name = name.substring(0, name.indexOf('(') - 1);
}
// Add host+port
String host = descriptorURL.getHost();
int port = descriptorURL.getPort();
name = name + " (" + host + ":" + String.valueOf(port) + ")";
Map<String, Object> properties = new TreeMap<>();
properties.put(CONFIG_HOST, host);
properties.put(CONFIG_HTTP_PORT, port);
return DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(name)
.withRepresentationProperty(UDN).build();
}
@Override
public @Nullable ThingUID getThingUID(RemoteDevice device) {
DeviceDetails details = device.getDetails();
if (details != null && details.getManufacturerDetails() != null
&& "dresden elektronik".equals(details.getManufacturerDetails().getManufacturer())) {
return new ThingUID(BRIDGE_TYPE, details.getSerialNumber());
}
return null;
}
}

View File

@@ -0,0 +1,273 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.discovery;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Every bridge will add its discovered sensors to this discovery service to make them
* available to the framework.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ThingDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(LightThingHandler.SUPPORTED_THING_TYPE_UIDS, SensorThingHandler.SUPPORTED_THING_TYPES,
SensorThermostatThingHandler.SUPPORTED_THING_TYPES)
.flatMap(Set::stream).collect(Collectors.toSet());
private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
private @Nullable DeconzBridgeHandler handler;
private @Nullable ScheduledFuture<?> scanningJob;
private @Nullable ThingUID bridgeUID;
public ThingDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, 30);
}
@Override
protected void startScan() {
final DeconzBridgeHandler handler = this.handler;
if (handler != null) {
handler.requestFullState();
}
}
@Override
protected void startBackgroundDiscovery() {
final ScheduledFuture<?> scanningJob = this.scanningJob;
if (scanningJob == null || scanningJob.isCancelled()) {
this.scanningJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, 5, TimeUnit.MINUTES);
}
}
@Override
protected void stopBackgroundDiscovery() {
final ScheduledFuture<?> scanningJob = this.scanningJob;
if (scanningJob != null) {
scanningJob.cancel(true);
this.scanningJob = null;
}
}
/**
* Add a sensor device to the discovery inbox.
*
* @param lightID The id of the light
* @param light The sensor description
*/
private void addLight(String lightID, LightMessage light) {
final ThingUID bridgeUID = this.bridgeUID;
if (bridgeUID == null) {
logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
return;
}
ThingTypeUID thingTypeUID;
LightType lightType = light.type;
if (lightType == null) {
logger.warn("No light type reported for light {} ({})", light.modelid, light.name);
return;
}
Map<String, Object> properties = new HashMap<>();
properties.put("id", lightID);
properties.put(UNIQUE_ID, light.uniqueid);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, light.swversion);
properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
properties.put(Thing.PROPERTY_MODEL_ID, light.modelid);
if (light.ctmax != null && light.ctmin != null) {
properties.put(PROPERTY_CT_MAX,
Integer.toString(Util.constrainToRange(light.ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
properties.put(PROPERTY_CT_MIN,
Integer.toString(Util.constrainToRange(light.ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
}
switch (lightType) {
case ON_OFF_LIGHT:
case ON_OFF_PLUGIN_UNIT:
case SMART_PLUG:
thingTypeUID = THING_TYPE_ONOFF_LIGHT;
break;
case DIMMABLE_LIGHT:
case DIMMABLE_PLUGIN_UNIT:
thingTypeUID = THING_TYPE_DIMMABLE_LIGHT;
break;
case COLOR_TEMPERATURE_LIGHT:
thingTypeUID = THING_TYPE_COLOR_TEMPERATURE_LIGHT;
break;
case COLOR_DIMMABLE_LIGHT:
case COLOR_LIGHT:
thingTypeUID = THING_TYPE_COLOR_LIGHT;
break;
case EXTENDED_COLOR_LIGHT:
thingTypeUID = THING_TYPE_EXTENDED_COLOR_LIGHT;
break;
case WINDOW_COVERING_DEVICE:
thingTypeUID = THING_TYPE_WINDOW_COVERING;
break;
case WARNING_DEVICE:
thingTypeUID = THING_TYPE_WARNING_DEVICE;
break;
case CONFIGURATION_TOOL:
// ignore configuration tool device
return;
default:
logger.debug(
"Found light: {} ({}), type {} but no thing type defined for that type. This should be reported.",
light.modelid, light.name, light.type);
return;
}
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, light.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withLabel(light.name + " (" + light.manufacturername + ")").withProperties(properties)
.withRepresentationProperty(UNIQUE_ID).build();
thingDiscovered(discoveryResult);
}
/**
* Add a sensor device to the discovery inbox.
*
* @param sensorID The id of the sensor
* @param sensor The sensor description
*/
private void addSensor(String sensorID, SensorMessage sensor) {
final ThingUID bridgeUID = this.bridgeUID;
if (bridgeUID == null) {
logger.warn("Received a message from non-existent bridge. This most likely is a bug.");
return;
}
ThingTypeUID thingTypeUID;
if (sensor.type.contains("Daylight")) { // deCONZ specific: Software simulated daylight sensor
thingTypeUID = THING_TYPE_DAYLIGHT_SENSOR;
} else if (sensor.type.contains("Power")) { // ZHAPower, CLIPPower
thingTypeUID = THING_TYPE_POWER_SENSOR;
} else if (sensor.type.contains("ZHAConsumption")) { // ZHAConsumption
thingTypeUID = THING_TYPE_CONSUMPTION_SENSOR;
} else if (sensor.type.contains("Presence")) { // ZHAPresence, CLIPPrensence
thingTypeUID = THING_TYPE_PRESENCE_SENSOR;
} else if (sensor.type.contains("Switch")) { // ZHASwitch
if (sensor.modelid.contains("RGBW")) {
thingTypeUID = THING_TYPE_COLOR_CONTROL;
} else {
thingTypeUID = THING_TYPE_SWITCH;
}
} else if (sensor.type.contains("LightLevel")) { // ZHALightLevel
thingTypeUID = THING_TYPE_LIGHT_SENSOR;
} else if (sensor.type.contains("ZHATemperature")) { // ZHATemperature
thingTypeUID = THING_TYPE_TEMPERATURE_SENSOR;
} else if (sensor.type.contains("ZHAHumidity")) { // ZHAHumidity
thingTypeUID = THING_TYPE_HUMIDITY_SENSOR;
} else if (sensor.type.contains("ZHAPressure")) { // ZHAPressure
thingTypeUID = THING_TYPE_PRESSURE_SENSOR;
} else if (sensor.type.contains("ZHAOpenClose")) { // ZHAOpenClose
thingTypeUID = THING_TYPE_OPENCLOSE_SENSOR;
} else if (sensor.type.contains("ZHAWater")) { // ZHAWater
thingTypeUID = THING_TYPE_WATERLEAKAGE_SENSOR;
} else if (sensor.type.contains("ZHAFire")) {
thingTypeUID = THING_TYPE_FIRE_SENSOR; // ZHAFire
} else if (sensor.type.contains("ZHAAlarm")) {
thingTypeUID = THING_TYPE_ALARM_SENSOR; // ZHAAlarm
} else if (sensor.type.contains("ZHAVibration")) {
thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration
} else if (sensor.type.contains("ZHABattery")) {
thingTypeUID = THING_TYPE_BATTERY_SENSOR; // ZHABattery
} else if (sensor.type.contains("ZHAThermostat")) {
thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat
} else {
logger.debug("Unknown type {}", sensor.type);
return;
}
ThingUID uid = new ThingUID(thingTypeUID, bridgeUID, sensor.uniqueid.replaceAll("[^a-z0-9\\[\\]]", ""));
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID)
.withLabel(sensor.name + " (" + sensor.manufacturername + ")").withProperty("id", sensorID)
.withProperty(UNIQUE_ID, sensor.uniqueid).withRepresentationProperty(UNIQUE_ID).build();
thingDiscovered(discoveryResult);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof DeconzBridgeHandler) {
this.handler = (DeconzBridgeHandler) handler;
((DeconzBridgeHandler) handler).setDiscoveryService(this);
this.bridgeUID = handler.getThing().getUID();
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
/**
* Call this method when a full bridge state request has been performed and either the fullState
* are known or a failure happened.
*
* @param fullState The fullState or null.
*/
public void stateRequestFinished(final @Nullable BridgeFullState fullState) {
stopScan();
removeOlderResults(getTimestampOfLastScan());
if (fullState != null) {
fullState.sensors.forEach(this::addSensor);
fullState.lights.forEach(this::addLight);
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The success message for an API key request
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ApiKeyMessage {
public Success success = new Success();
public static class Success {
public String username = "";
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import java.util.Collections;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* http://dresden-elektronik.github.io/deconz-rest-doc/configuration/
* # Get full state
* GET /api/<apikey>
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class BridgeFullState {
public Config config = new Config();
public static class Config {
public String apiversion = ""; // "1.0.0"
public String ipaddress = ""; // "192.168.80.142",
public String name = ""; // "deCONZ-GW",
public String swversion = ""; // "20405"
public String fwversion = ""; // "0x262e0500"
public String uuid = ""; // "a65d80a1-975a-4598-8d5a-2547bc18d63b",
public int websocketport = 0; // 8088
public int zigbeechannel = 0;
}
public Map<String, SensorMessage> sensors = Collections.emptyMap();
public Map<String, LightMessage> lights = Collections.emptyMap();
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The REST interface and websocket connection are using the same fields.
* The REST data contains more descriptive info like the manufacturer and name.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class DeconzBaseMessage {
// For websocket change events
public String e = ""; // "changed"
public String r = ""; // "sensors"
public String t = ""; // "event"
public String id = ""; // "3"
// for rest API
public String manufacturername = "";
public String modelid = "";
public String name = "";
public String swversion = "";
/** the API endpoint **/
public String ep = "";
/** device last seen */
public @Nullable String lastseen;
// websocket and rest api
public String uniqueid = ""; // "00:0b:57:ff:fe:94:6b:dd-01-1000"
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.types.LightType;
/**
* The REST interface and websocket connection are using the same fields.
* The REST data contains more descriptive info like the manufacturer and name.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class LightMessage extends DeconzBaseMessage {
public @Nullable Boolean hascolor;
public @Nullable Integer ctmax;
public @Nullable Integer ctmin;
public @Nullable LightType type;
public @Nullable LightState state;
@Override
public String toString() {
return "LightMessage{" + "hascolor=" + hascolor + ", ctmax=" + ctmax + ", ctmin=" + ctmin + ", type=" + type
+ ", state=" + state + ", e='" + e + '\'' + ", r='" + r + '\'' + ", t='" + t + '\'' + ", id='" + id
+ '\'' + ", manufacturername='" + manufacturername + '\'' + ", modelid='" + modelid + '\'' + ", name='"
+ name + '\'' + ", swversion='" + swversion + '\'' + ", ep='" + ep + '\'' + ", uniqueid='" + uniqueid
+ '\'' + '}';
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link LightState} is send by the websocket connection as well as the Rest API.
* It is part of a {@link LightMessage}.
*
* This should be in sync with the supported lights from
* https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class LightState {
public @Nullable Boolean reachable;
public @Nullable Boolean on;
public @Nullable Integer bri;
public @Nullable String alert;
public @Nullable String colormode;
public @Nullable String effect;
// depending on the type of light
public @Nullable Integer hue;
public @Nullable Integer sat;
public @Nullable Integer ct;
public double @Nullable [] xy;
public @Nullable Integer transitiontime;
/**
* compares two light states and ignore all fields that are null in either state
*
* @param o state to compare with
* @return true if equal
*/
public boolean equalsIgnoreNull(LightState o) {
return equalsIgnoreNull(on, o.on) && equalsIgnoreNull(bri, o.bri) && equalsIgnoreNull(hue, o.hue)
&& equalsIgnoreNull(sat, o.sat) && ((xy != null && o.xy != null) ? Arrays.equals(xy, o.xy) : true);
}
/**
* clear this light state
*/
public void clear() {
reachable = null;
on = null;
bri = null;
alert = null;
colormode = null;
effect = null;
hue = null;
sat = null;
ct = null;
xy = null;
transitiontime = null;
}
private <T> boolean equalsIgnoreNull(T o1, T o2) {
return (o1 != null && o2 != null) ? o1.equals(o2) : true;
}
@Override
public String toString() {
return "LightState{reachable=" + reachable + ", on=" + on + ", bri=" + bri + ", alert='" + alert + '\''
+ ", colormode='" + colormode + '\'' + ", effect='" + effect + '\'' + ", hue=" + hue + ", sat=" + sat
+ ", ct=" + ct + ", xy=" + Arrays.toString(xy) + ", transitiontime=" + transitiontime + '}';
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
/**
* The {@link SensorConfig} is send by the the Rest API.
* It is part of a {@link SensorMessage}.
*
* This should be in sync with the supported sensors from
* https://dresden-elektronik.github.io/deconz-rest-doc/sensors/.
*
* @author David Graeff - Initial contribution
* @author Lukas Agethen - Add Thermostat parameters
*/
@NonNullByDefault
public class SensorConfig {
public boolean on = true;
public boolean reachable = true;
public @Nullable Integer battery;
public @Nullable Float temperature;
public @Nullable Integer heatsetpoint;
public @Nullable ThermostatMode mode;
public @Nullable Integer offset;
@Override
public String toString() {
return "SensorConfig{" + "on=" + on + ", reachable=" + reachable + ", battery=" + battery + ", temperature="
+ temperature + ", heatsetpoint=" + heatsetpoint + ", mode=" + mode + ", offset=" + offset + '}';
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The REST interface and websocket connection are using the same fields.
* The REST data contains more descriptive info like the manufacturer and name.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class SensorMessage extends DeconzBaseMessage {
public String type = "";
public @Nullable SensorConfig config;
public @Nullable SensorState state;
@Override
public String toString() {
return "SensorMessage{" + "type='" + type + '\'' + ", config=" + config + ", state=" + state + ", e='" + e
+ '\'' + ", r='" + r + '\'' + ", t='" + t + '\'' + ", id='" + id + '\'' + ", manufacturername='"
+ manufacturername + '\'' + ", modelid='" + modelid + '\'' + ", name='" + name + '\'' + ", swversion='"
+ swversion + '\'' + ", ep='" + ep + '\'' + ", lastseen='" + lastseen + '\'' + ", uniqueid='" + uniqueid
+ '\'' + '}';
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link SensorState} is send by the websocket connection as well as the Rest API.
* It is part of a {@link SensorMessage}.
*
* This should be in sync with the supported sensors from
* https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class SensorState {
/** Some presence sensors, the daylight sensor and all light sensors provide the "dark" boolean. */
public @Nullable Boolean dark;
/** The daylight sensor and all light sensors provides the "daylight" boolean. */
public @Nullable Boolean daylight;
/** Light sensors provide a light level value. */
public @Nullable Integer lightlevel;
/** Light sensors provide a lux value. */
public @Nullable Integer lux;
/** Temperature sensors provide a degrees value. */
public @Nullable Float temperature;
/** Humidity sensors provide a percent value. */
public @Nullable Float humidity;
/** OpenClose sensors provide a boolean value. */
public @Nullable Boolean open;
/** fire sensors provide a boolean value. */
public @Nullable Boolean fire;
/** water sensors provide a boolean value. */
public @Nullable Boolean water;
/** alarm sensors provide a boolean value. */
public @Nullable Boolean alarm;
/** IAS Zone sensors provide a boolean value. */
public @Nullable Boolean tampered;
/** vibration sensors provide a boolean value. */
public @Nullable Boolean vibration;
/** carbonmonoxide sensors provide a boolean value. */
public @Nullable Boolean carbonmonoxide;
/** Pressure sensors provide a hPa value. */
public @Nullable Integer pressure;
/** Presence sensors provide this boolean. */
public @Nullable Boolean presence;
/** Power sensors provide this value in Watts. */
public @Nullable Float power;
/** Batttery sensors provide this value */
public @Nullable Integer battery;
/** Consumption sensors provide this value in Watts/hour. */
public @Nullable Float consumption;
/** Power sensors provide this value in Volt. */
public @Nullable Float voltage;
/** Power sensors provide this value in Milliampere. */
public @Nullable Float current;
/** Light sensors and the daylight sensor provide a status integer that can have various semantics. */
public @Nullable Integer status;
/** Switches provide this value. */
public @Nullable Integer buttonevent;
/** Switches may provide this value. */
public @Nullable Integer gesture;
/** Thermostat may provide this value. */
public @Nullable Integer valve;
/** deCONZ sends a last update string with every event. */
public @Nullable String lastupdated;
/** color controllers send xy values */
public double @Nullable [] xy;
@Override
public String toString() {
return "SensorState{" + "dark=" + dark + ", daylight=" + daylight + ", lightlevel=" + lightlevel + ", lux="
+ lux + ", temperature=" + temperature + ", humidity=" + humidity + ", open=" + open + ", fire=" + fire
+ ", water=" + water + ", alarm=" + alarm + ", tampered=" + tampered + ", vibration=" + vibration
+ ", carbonmonoxide=" + carbonmonoxide + ", pressure=" + pressure + ", presence=" + presence
+ ", power=" + power + ", battery=" + battery + ", consumption=" + consumption + ", voltage=" + voltage
+ ", current=" + current + ", status=" + status + ", buttonevent=" + buttonevent + ", gesture="
+ gesture + ", valve=" + valve + ", lastupdated='" + lastupdated + '\'' + ", xy=" + Arrays.toString(xy)
+ '}';
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
/**
* The {@link ThermostatConfig} is send to the Rest API to configure Thermostat.
*
* @author Lukas Agethen - Initial contribution
*/
@NonNullByDefault
public class ThermostatConfig {
public @Nullable Integer heatsetpoint;
public @Nullable ThermostatMode mode;
public @Nullable Integer offset;
}

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.Util.buildUrl;
import java.net.SocketTimeoutException;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
import org.openhab.binding.deconz.internal.netutils.WebSocketMessageListener;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* This base thing doesn't establish any connections, that is done by the bridge Thing.
*
* It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener.
*
* A REST API call is made to get the initial light/rollershutter state.
*
* @author David Graeff - Initial contribution
* @author Jan N. Klug - Refactored to abstract class
*/
@NonNullByDefault
public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extends BaseThingHandler
implements WebSocketMessageListener {
private final Logger logger = LoggerFactory.getLogger(DeconzBaseThingHandler.class);
protected ThingConfig config = new ThingConfig();
protected DeconzBridgeConfig bridgeConfig = new DeconzBridgeConfig();
protected final Gson gson;
private @Nullable ScheduledFuture<?> initializationJob;
protected @Nullable WebSocketConnection connection;
protected @Nullable AsyncHttpClient http;
public DeconzBaseThingHandler(Thing thing, Gson gson) {
super(thing);
this.gson = gson;
}
/**
* Stops the API request
*/
private void stopInitializationJob() {
ScheduledFuture<?> future = initializationJob;
if (future != null) {
future.cancel(true);
initializationJob = null;
}
}
protected abstract void registerListener();
protected abstract void unregisterListener();
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (config.id.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "ID not set");
return;
}
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
unregisterListener();
return;
}
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
return;
}
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
DeconzBridgeHandler bridgeHandler = (DeconzBridgeHandler) bridge.getHandler();
if (bridgeHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
}
final WebSocketConnection webSocketConnection = bridgeHandler.getWebsocketConnection();
this.connection = webSocketConnection;
this.http = bridgeHandler.getHttp();
this.bridgeConfig = bridgeHandler.getBridgeConfig();
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);
// Real-time data
registerListener();
// get initial values
requestState();
}
protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
/**
* processes a newly received state response
*
* MUST set the thing status!
*
* @param stateResponse
*/
protected abstract void processStateResponse(@Nullable T stateResponse);
/**
* call requestState(type) in this method only
*/
protected abstract void requestState();
/**
* Perform a request to the REST API for retrieving the full light state with all data and configuration.
*/
protected void requestState(String type) {
AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) {
return;
}
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, type, config.id);
logger.trace("Requesting URL for initial data: {}", url);
// Get initial data
asyncHttpClient.get(url, bridgeConfig.timeout).thenApply(this::parseStateResponse).exceptionally(e -> {
if (e instanceof SocketTimeoutException || e instanceof TimeoutException
|| e instanceof CompletionException) {
logger.debug("Get new state failed: ", e);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
stopInitializationJob();
initializationJob = scheduler.schedule((Runnable) this::requestState, 10, TimeUnit.SECONDS);
return null;
}).thenAccept(this::processStateResponse);
}
@Override
public void dispose() {
stopInitializationJob();
WebSocketConnection webSocketConnection = connection;
if (webSocketConnection != null) {
webSocketConnection.unregisterLightListener(config.id);
}
super.dispose();
}
@Override
public void initialize() {
config = getConfigAs(ThingConfig.class);
Bridge bridge = getBridge();
if (bridge != null) {
bridgeStatusChanged(bridge.getStatusInfo());
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link DeconzBridgeConfig} class holds the configuration properties of the bridge.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class DeconzBridgeConfig {
public String host = "";
public int httpPort = 80;
public int port = 0;
public @Nullable String apikey;
int timeout = 2000;
public String getHostWithoutPort() {
String hostWithoutPort = host;
if (hostWithoutPort.indexOf(':') > 0) {
hostWithoutPort = hostWithoutPort.substring(0, hostWithoutPort.indexOf(':'));
}
return hostWithoutPort;
}
}

View File

@@ -0,0 +1,331 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import static org.openhab.binding.deconz.internal.Util.buildUrl;
import java.net.SocketTimeoutException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService;
import org.openhab.binding.deconz.internal.dto.ApiKeyMessage;
import org.openhab.binding.deconz.internal.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnectionListener;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The bridge Thing is responsible for requesting all available sensors and switches and propagate
* them to the discovery service.
*
* It performs the authorization process if necessary.
*
* A websocket connection is established to the deCONZ software and kept alive.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketConnectionListener {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
private final Logger logger = LoggerFactory.getLogger(DeconzBridgeHandler.class);
private @Nullable ThingDiscoveryService thingDiscoveryService;
private final WebSocketConnection websocket;
private final AsyncHttpClient http;
private DeconzBridgeConfig config = new DeconzBridgeConfig();
private final Gson gson;
private @Nullable ScheduledFuture<?> scheduledFuture;
private int websocketPort = 0;
/** Prevent a dispose/init cycle while this flag is set. Use for property updates */
private boolean ignoreConfigurationUpdate;
private boolean websocketReconnect = false;
/** The poll frequency for the API Key verification */
private static final int POLL_FREQUENCY_SEC = 10;
public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) {
super(thing);
this.http = http;
this.gson = gson;
String websocketID = thing.getUID().getAsString().replace(':', '-');
websocketID = websocketID.length() < 3 ? websocketID : websocketID.substring(websocketID.length() - 20);
this.websocket = new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ThingDiscoveryService.class);
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
if (!ignoreConfigurationUpdate) {
super.handleConfigurationUpdate(configurationParameters);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
/**
* Stops the API request or websocket reconnect timer
*/
private void stopTimer() {
ScheduledFuture<?> future = scheduledFuture;
if (future != null) {
future.cancel(true);
scheduledFuture = null;
}
}
/**
* Parses the response message to the API key generation REST API.
*
* @param r The response
*/
private void parseAPIKeyResponse(AsyncHttpClient.Result r) {
if (r.getResponseCode() == 403) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
"Allow authentification for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
stopTimer();
scheduledFuture = scheduler.schedule(() -> requestApiKey(), POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
} else if (r.getResponseCode() == 200) {
ApiKeyMessage[] response = gson.fromJson(r.getBody(), ApiKeyMessage[].class);
if (response.length == 0) {
throw new IllegalStateException("Authorisation request response is empty");
}
config.apikey = response[0].success.username;
Configuration configuration = editConfiguration();
configuration.put(CONFIG_APIKEY, config.apikey);
updateConfiguration(configuration);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for configuration");
requestFullState();
} else {
throw new IllegalStateException("Unknown status code for authorisation request");
}
}
/**
* Parses the response message to the REST API for retrieving the full bridge state with all sensors and switches
* and configuration.
*
* @param r The response
*/
private @Nullable BridgeFullState parseBridgeFullStateResponse(AsyncHttpClient.Result r) {
if (r.getResponseCode() == 403) {
return null;
} else if (r.getResponseCode() == 200) {
return gson.fromJson(r.getBody(), BridgeFullState.class);
} else {
throw new IllegalStateException("Unknown status code for full state request");
}
}
/**
* Perform a request to the REST API for retrieving the full bridge state with all sensors and switches
* and configuration.
*/
public void requestFullState() {
if (config.apikey == null) {
return;
}
String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey);
http.get(url, config.timeout).thenApply(this::parseBridgeFullStateResponse).exceptionally(e -> {
if (e instanceof SocketTimeoutException || e instanceof TimeoutException
|| e instanceof CompletionException) {
logger.debug("Get full state failed", e);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
return null;
}).whenComplete((value, error) -> {
final ThingDiscoveryService thingDiscoveryService = this.thingDiscoveryService;
if (thingDiscoveryService != null) {
// Hand over sensors to discovery service
thingDiscoveryService.stateRequestFinished(value);
}
}).thenAccept(fullState -> {
if (fullState == null) {
return;
}
if (fullState.config.name.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"You are connected to a HUE bridge, not a deCONZ software!");
return;
}
if (fullState.config.websocketport == 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"deCONZ software too old. No websocket support!");
return;
}
// Add some information about the bridge
Map<String, String> editProperties = editProperties();
editProperties.put("apiversion", fullState.config.apiversion);
editProperties.put("swversion", fullState.config.swversion);
editProperties.put("fwversion", fullState.config.fwversion);
editProperties.put("uuid", fullState.config.uuid);
editProperties.put("zigbeechannel", String.valueOf(fullState.config.zigbeechannel));
editProperties.put("ipaddress", fullState.config.ipaddress);
ignoreConfigurationUpdate = true;
updateProperties(editProperties);
ignoreConfigurationUpdate = false;
// Use requested websocket port if no specific port is given
websocketPort = config.port == 0 ? fullState.config.websocketport : config.port;
websocketReconnect = true;
startWebsocket();
}).exceptionally(e -> {
if (e != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
}
logger.warn("Full state parsing failed", e);
return null;
});
}
/**
* Starts the websocket connection.
* {@link #requestFullState} need to be called first to obtain the websocket port.
*/
private void startWebsocket() {
if (websocket.isConnected() || websocketPort == 0) {
return;
}
stopTimer();
scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
websocket.start(config.getHostWithoutPort() + ":" + websocketPort);
}
/**
* Perform a request to the REST API for generating an API key.
*
*/
private CompletableFuture<?> requestApiKey() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Requesting API Key");
stopTimer();
String url = buildUrl(config.getHostWithoutPort(), config.httpPort);
return http.post(url, "{\"devicetype\":\"openHAB\"}", config.timeout).thenAccept(this::parseAPIKeyResponse)
.exceptionally(e -> {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
logger.warn("Authorisation failed", e);
return null;
});
}
@Override
public void initialize() {
logger.debug("Start initializing!");
config = getConfigAs(DeconzBridgeConfig.class);
if (config.apikey == null) {
requestApiKey();
} else {
requestFullState();
}
}
@Override
public void dispose() {
websocketReconnect = false;
stopTimer();
websocket.close();
}
@Override
public void connectionError(@Nullable Throwable e) {
if (e != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason");
}
stopTimer();
// Wait for POLL_FREQUENCY_SEC after a connection error before trying again
if (websocketReconnect) {
scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
}
}
@Override
public void connectionEstablished() {
stopTimer();
updateStatus(ThingStatus.ONLINE);
}
@Override
public void connectionLost(String reason) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
if (websocketReconnect) {
startWebsocket();
}
}
/**
* Return the websocket connection.
*/
public WebSocketConnection getWebsocketConnection() {
return websocket;
}
/**
* Return the http connection.
*/
public AsyncHttpClient getHttp() {
return http;
}
/**
* Return the bridge configuration.
*/
public DeconzBridgeConfig getBridgeConfig() {
return config;
}
/**
* Called by the {@link ThingDiscoveryService}. Informs the bridge handler about the service.
*
* @param thingDiscoveryService The service
*/
public void setDiscoveryService(ThingDiscoveryService thingDiscoveryService) {
this.thingDiscoveryService = thingDiscoveryService;
}
}

View File

@@ -0,0 +1,405 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import static org.openhab.binding.deconz.internal.Util.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.LightState;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
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.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* This light thing doesn't establish any connections, that is done by the bridge Thing.
*
* It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener.
*
* A REST API call is made to get the initial light/rollershutter state.
*
* Every light and rollershutter is supported by this Thing, because a unified state is kept
* in {@link #lightStateCache}. Every field that got received by the REST API for this specific
* sensor is published to the framework.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Stream.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT,
THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ONOFF_LIGHT,
THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE).collect(Collectors.toSet());
private static final double HUE_FACTOR = 65535 / 360.0;
private static final double BRIGHTNESS_FACTOR = 2.54;
private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms
private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
private final StateDescriptionProvider stateDescriptionProvider;
private long lastCommandExpireTimestamp = 0;
private boolean needsPropertyUpdate = false;
/**
* The light state. Contains all possible fields for all supported lights
*/
private LightState lightStateCache = new LightState();
private LightState lastCommand = new LightState();
// set defaults, we can override them later if we receive better values
private int ctMax = ZCL_CT_MAX;
private int ctMin = ZCL_CT_MIN;
public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
super(thing, gson);
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
public void initialize() {
if (thing.getThingTypeUID().equals(THING_TYPE_COLOR_TEMPERATURE_LIGHT)
|| thing.getThingTypeUID().equals(THING_TYPE_EXTENDED_COLOR_LIGHT)) {
try {
Map<String, String> properties = thing.getProperties();
ctMax = Integer.parseInt(properties.get(PROPERTY_CT_MAX));
ctMin = Integer.parseInt(properties.get(PROPERTY_CT_MIN));
// minimum and maximum are inverted due to mired/kelvin conversion!
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withMinimum(new BigDecimal(miredToKelvin(ctMax)))
.withMaximum(new BigDecimal(miredToKelvin(ctMin))).build().toStateDescription();
if (stateDescription != null) {
stateDescriptionProvider.setDescription(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE),
stateDescription);
} else {
logger.warn("Failed to create state description in thing {}", thing.getUID());
}
} catch (NumberFormatException e) {
needsPropertyUpdate = true;
}
}
super.initialize();
}
@Override
protected void registerListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.registerLightListener(config.id, this);
}
}
@Override
protected void unregisterListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.unregisterLightListener(config.id);
}
}
@Override
protected void requestState() {
requestState("lights");
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
valueUpdated(channelUID.getId(), lightStateCache);
return;
}
LightState newLightState = new LightState();
Boolean currentOn = lightStateCache.on;
Integer currentBri = lightStateCache.bri;
switch (channelUID.getId()) {
case CHANNEL_ALERT:
if (command instanceof OnOffType) {
newLightState.alert = command == OnOffType.ON ? "alert" : "none";
} else {
return;
}
case CHANNEL_SWITCH:
if (command instanceof OnOffType) {
newLightState.on = (command == OnOffType.ON);
} else {
return;
}
break;
case CHANNEL_BRIGHTNESS:
case CHANNEL_COLOR:
if (command instanceof OnOffType) {
newLightState.on = (command == OnOffType.ON);
} else if (command instanceof HSBType) {
HSBType hsbCommand = (HSBType) command;
if ("xy".equals(lightStateCache.colormode)) {
PercentType[] xy = hsbCommand.toXY();
if (xy.length < 2) {
logger.warn("Failed to convert {} to xy-values", command);
}
newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
newLightState.bri = fromPercentType(hsbCommand.getBrightness());
} else {
// default is colormode "hs" (used when colormode "hs" is set or colormode is unknown)
newLightState.bri = fromPercentType(hsbCommand.getBrightness());
newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
newLightState.sat = fromPercentType(hsbCommand.getSaturation());
}
} else if (command instanceof PercentType) {
newLightState.bri = fromPercentType((PercentType) command);
} else if (command instanceof DecimalType) {
newLightState.bri = ((DecimalType) command).intValue();
} else {
return;
}
// send on/off state together with brightness if not already set or unknown
Integer newBri = newLightState.bri;
if ((newBri != null) && ((currentOn == null) || ((newBri > 0) != currentOn))) {
newLightState.on = (newBri > 0);
}
// fix sending bri=0 when light is already off
if (newBri != null && newBri == 0 && currentOn != null && !currentOn) {
return;
}
Double transitiontime = config.transitiontime;
if (transitiontime != null) {
// value is in 1/10 seconds
newLightState.transitiontime = (int) Math.round(10 * transitiontime);
}
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof DecimalType) {
int miredValue = kelvinToMired(((DecimalType) command).intValue());
newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
if (currentOn != null && !currentOn) {
// sending new color temperature is only allowed when light is on
newLightState.on = true;
}
} else {
return;
}
break;
case CHANNEL_POSITION:
if (command instanceof UpDownType) {
newLightState.on = (command == UpDownType.DOWN);
} else if (command == StopMoveType.STOP) {
if (currentOn != null && currentOn && currentBri != null && currentBri <= 254) {
// going down or currently stop (254 because of rounding error)
newLightState.on = true;
} else if (currentOn != null && !currentOn && currentBri != null && currentBri > 0) {
// going up or currently stopped
newLightState.on = false;
}
} else if (command instanceof PercentType) {
newLightState.bri = fromPercentType((PercentType) command);
} else {
return;
}
break;
default:
// no supported command
return;
}
AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) {
return;
}
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "lights", config.id,
"state");
if (newLightState.on != null && !newLightState.on) {
// if light shall be off, no other commands are allowed, so reset the new light state
newLightState.clear();
newLightState.on = false;
}
String json = gson.toJson(newLightState);
logger.trace("Sending {} to light {} via {}", json, config.id, url);
asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
lastCommandExpireTimestamp = System.currentTimeMillis()
+ (newLightState.transitiontime != null ? newLightState.transitiontime
: DEFAULT_COMMAND_EXPIRY_TIME);
lastCommand = newLightState;
logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody());
}).exceptionally(e -> {
logger.debug("Sending command {} to channel {} failed:", command, channelUID, e);
return null;
});
}
@Override
protected @Nullable LightMessage parseStateResponse(AsyncHttpClient.Result r) {
if (r.getResponseCode() == 403) {
return null;
} else if (r.getResponseCode() == 200) {
LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
if (lightMessage != null && needsPropertyUpdate) {
// if we did not receive an ctmin/ctmax, then we probably don't need it
needsPropertyUpdate = false;
if (lightMessage.ctmin != null && lightMessage.ctmax != null) {
Map<String, String> properties = new HashMap<>(thing.getProperties());
properties.put(PROPERTY_CT_MAX,
Integer.toString(Util.constrainToRange(lightMessage.ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
properties.put(PROPERTY_CT_MIN,
Integer.toString(Util.constrainToRange(lightMessage.ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
updateProperties(properties);
}
}
return lightMessage;
} else {
throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
}
}
@Override
protected void processStateResponse(@Nullable LightMessage stateResponse) {
if (stateResponse == null) {
return;
}
messageReceived(config.id, stateResponse);
}
private void valueUpdated(String channelId, LightState newState) {
Integer bri = newState.bri;
Boolean on = newState.on;
switch (channelId) {
case CHANNEL_ALERT:
updateState(channelId, "alert".equals(newState.alert) ? OnOffType.ON : OnOffType.OFF);
break;
case CHANNEL_SWITCH:
if (on != null) {
updateState(channelId, OnOffType.from(on));
}
break;
case CHANNEL_COLOR:
if (on != null && on == false) {
updateState(channelId, OnOffType.OFF);
} else if (bri != null && newState.colormode != null && newState.colormode.equals("xy")) {
final double @Nullable [] xy = newState.xy;
if (xy != null && xy.length == 2) {
HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
}
} else if (bri != null && newState.hue != null && newState.sat != null) {
final Integer hue = newState.hue;
final Integer sat = newState.sat;
updateState(channelId,
new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
}
break;
case CHANNEL_BRIGHTNESS:
if (bri != null && on != null && on) {
updateState(channelId, toPercentType(bri));
} else {
updateState(channelId, OnOffType.OFF);
}
break;
case CHANNEL_COLOR_TEMPERATURE:
Integer ct = newState.ct;
if (ct != null && ct >= ctMin && ct <= ctMax) {
updateState(channelId, new DecimalType(miredToKelvin(ct)));
}
break;
case CHANNEL_POSITION:
if (bri != null) {
updateState(channelId, toPercentType(bri));
}
default:
}
}
@Override
public void messageReceived(String sensorID, DeconzBaseMessage message) {
if (message instanceof LightMessage) {
LightMessage lightMessage = (LightMessage) message;
logger.trace("{} received {}", thing.getUID(), lightMessage);
LightState lightState = lightMessage.state;
if (lightState != null) {
if (lastCommandExpireTimestamp > System.currentTimeMillis()
&& !lightState.equalsIgnoreNull(lastCommand)) {
// skip for SKIP_UPDATE_TIMESPAN after last command if lightState is different from command
logger.trace("Ignoring differing update after last command until {}", lastCommandExpireTimestamp);
return;
}
lightStateCache = lightState;
if (lightState.reachable != null && lightState.reachable) {
updateStatus(ThingStatus.ONLINE);
thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState));
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
thing.getChannels().stream().map(c -> c.getUID()).forEach(c -> updateState(c, UnDefType.UNDEF));
}
}
}
}
private PercentType toPercentType(int val) {
int scaledValue = (int) Math.ceil(val / BRIGHTNESS_FACTOR);
if (scaledValue < 0 || scaledValue > 100) {
logger.trace("received value {} (converted to {}). Coercing.", val, scaledValue);
scaledValue = scaledValue < 0 ? 0 : scaledValue;
scaledValue = scaledValue > 100 ? 100 : scaledValue;
}
logger.debug("val = '{}', scaledValue = '{}'", val, scaledValue);
return new PercentType(scaledValue);
}
private int fromPercentType(PercentType val) {
return (int) Math.floor(val.doubleValue() * BRIGHTNESS_FACTOR);
}
}

View File

@@ -0,0 +1,323 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
*
* It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener.
*
* A REST API call is made to get the initial sensor state.
*
* Every sensor and switch is supported by this Thing, because a unified state is kept
* in {@link #sensorState}. Every field that got received by the REST API for this specific
* sensor is published to the framework.
*
* @author David Graeff - Initial contribution
* @author Lukas Agethen - Refactored to provide better extensibility
*/
@NonNullByDefault
public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<SensorMessage> {
private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class);
/**
* The sensor state. Contains all possible fields for all supported sensors and switches
*/
protected SensorConfig sensorConfig = new SensorConfig();
protected SensorState sensorState = new SensorState();
/**
* Prevent a dispose/init cycle while this flag is set. Use for property updates
*/
private boolean ignoreConfigurationUpdate;
private @Nullable ScheduledFuture<?> lastSeenPollingJob;
public SensorBaseThingHandler(Thing thing, Gson gson) {
super(thing, gson);
}
@Override
protected void requestState() {
requestState("sensors");
}
@Override
protected void registerListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.registerSensorListener(config.id, this);
}
}
@Override
protected void unregisterListener() {
WebSocketConnection conn = connection;
if (conn != null) {
conn.unregisterSensorListener(config.id);
}
}
@Override
public void dispose() {
ScheduledFuture<?> lastSeenPollingJob = this.lastSeenPollingJob;
if (lastSeenPollingJob != null) {
lastSeenPollingJob.cancel(true);
this.lastSeenPollingJob = null;
}
super.dispose();
}
@Override
public abstract void handleCommand(ChannelUID channelUID, Command command);
protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig);
protected abstract List<String> getConfigChannels();
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
if (!ignoreConfigurationUpdate) {
super.handleConfigurationUpdate(configurationParameters);
}
}
@Override
protected @Nullable SensorMessage parseStateResponse(AsyncHttpClient.Result r) {
if (r.getResponseCode() == 403) {
return null;
} else if (r.getResponseCode() == 200) {
return gson.fromJson(r.getBody(), SensorMessage.class);
} else {
throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
}
}
@Override
protected void processStateResponse(@Nullable SensorMessage stateResponse) {
logger.trace("{} received {}", thing.getUID(), stateResponse);
if (stateResponse == null) {
return;
}
SensorConfig newSensorConfig = stateResponse.config;
sensorConfig = newSensorConfig != null ? newSensorConfig : new SensorConfig();
SensorState newSensorState = stateResponse.state;
sensorState = newSensorState != null ? newSensorState : new SensorState();
// Add some information about the sensor
if (!sensorConfig.reachable) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
return;
}
if (!sensorConfig.on) {
updateStatus(ThingStatus.OFFLINE);
return;
}
Map<String, String> editProperties = editProperties();
editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, stateResponse.swversion);
editProperties.put(Thing.PROPERTY_MODEL_ID, stateResponse.modelid);
editProperties.put(UNIQUE_ID, stateResponse.uniqueid);
ignoreConfigurationUpdate = true;
updateProperties(editProperties);
// Some sensors support optional channels
// (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors)
// any battery-powered sensor
if (sensorConfig.battery != null) {
createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE);
createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE);
}
createTypeSpecificChannels(sensorConfig, sensorState);
ignoreConfigurationUpdate = false;
// Initial data
updateChannels(sensorConfig);
updateChannels(sensorState, true);
// "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
// For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
// So to monitor a sensor is still alive, the "last seen" is necessary.
String lastSeen = stateResponse.lastseen;
if (lastSeen != null && config.lastSeenPolling > 0) {
createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
// Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
// manually poll it after the defined time (default is off)
if (config.lastSeenPolling > 0) {
lastSeenPollingJob = scheduler.schedule((Runnable) this::requestState, config.lastSeenPolling,
TimeUnit.MINUTES);
logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
config.lastSeenPolling);
}
}
updateStatus(ThingStatus.ONLINE);
}
protected void createChannel(String channelId, ChannelKind kind) {
ThingHandlerCallback callback = getCallback();
if (callback != null) {
ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId);
ChannelTypeUID channelTypeUID;
switch (channelId) {
case CHANNEL_BATTERY_LEVEL:
channelTypeUID = new ChannelTypeUID("system:battery-level");
break;
case CHANNEL_BATTERY_LOW:
channelTypeUID = new ChannelTypeUID("system:low-battery");
break;
default:
channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId);
break;
}
Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build();
updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build());
}
}
/**
* Update channel value from {@link SensorConfig} object - override to include further channels
*
* @param channelUID
* @param newConfig
*/
protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
Integer batteryLevel = newConfig.battery;
switch (channelUID.getId()) {
case CHANNEL_BATTERY_LEVEL:
if (batteryLevel != null) {
updateState(channelUID, new DecimalType(batteryLevel.longValue()));
}
break;
case CHANNEL_BATTERY_LOW:
if (batteryLevel != null) {
updateState(channelUID, OnOffType.from(batteryLevel <= 10));
}
break;
default:
// other cases covered by sub-class
}
}
/**
* Update channel value from {@link SensorState} object - override to include further channels
*
* @param channelID
* @param newState
* @param initializing
*/
protected void valueUpdated(String channelID, SensorState newState, boolean initializing) {
switch (channelID) {
case CHANNEL_LAST_UPDATED:
String lastUpdated = newState.lastupdated;
if (lastUpdated != null && !"none".equals(lastUpdated)) {
updateState(channelID, Util.convertTimestampToDateTime(lastUpdated));
}
break;
default:
// other cases covered by sub-class
}
}
@Override
public void messageReceived(String sensorID, DeconzBaseMessage message) {
logger.trace("{} received {}", thing.getUID(), message);
if (message instanceof SensorMessage) {
SensorMessage sensorMessage = (SensorMessage) message;
SensorConfig sensorConfig = sensorMessage.config;
if (sensorConfig != null) {
this.sensorConfig = sensorConfig;
updateChannels(sensorConfig);
}
SensorState sensorState = sensorMessage.state;
if (sensorState != null) {
updateChannels(sensorState, false);
}
}
}
private void updateChannels(SensorConfig newConfig) {
List<String> configChannels = getConfigChannels();
thing.getChannels().stream().map(Channel::getUID)
.filter(channelUID -> configChannels.contains(channelUID.getId()))
.forEach((channelUID) -> valueUpdated(channelUID, newConfig));
}
protected void updateChannels(SensorState newState, boolean initializing) {
sensorState = newState;
thing.getChannels().forEach(channel -> valueUpdated(channel.getUID().getId(), newState, initializing));
}
protected void updateSwitchChannel(String channelID, @Nullable Boolean value) {
if (value == null) {
return;
}
updateState(channelID, OnOffType.from(value));
}
protected void updateDecimalTypeChannel(String channelID, @Nullable Number value) {
if (value == null) {
return;
}
updateState(channelID, new DecimalType(value.longValue()));
}
protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit<?> unit) {
updateQuantityTypeChannel(channelID, value, unit, 1.0);
}
protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit<?> unit, double scaling) {
if (value == null) {
return;
}
updateState(channelID, new QuantityType<>(value.doubleValue() * scaling, unit));
}
}

View File

@@ -0,0 +1,199 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import static org.openhab.binding.deconz.internal.Util.buildUrl;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.SmartHomeUnits.PERCENT;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.binding.deconz.internal.dto.ThermostatConfig;
import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* This sensor Thermostat Thing doesn't establish any connections, that is done by the bridge Thing.
*
* It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener.
*
* A REST API call is made to get the initial sensor state.
*
* Only the Thermostat is supported by this Thing, because a unified state is kept
* in {@link #sensorState}. Every field that got received by the REST API for this specific
* sensor is published to the framework.
*
* @author Lukas Agethen - Initial contribution
*/
@NonNullByDefault
public class SensorThermostatThingHandler extends SensorBaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_THERMOSTAT);
private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE);
private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class);
public SensorThermostatThingHandler(Thing thing, Gson gson) {
super(thing, gson);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
sensorState.buttonevent = null;
valueUpdated(channelUID.getId(), sensorState, false);
return;
}
ThermostatConfig newConfig = new ThermostatConfig();
String channelId = channelUID.getId();
switch (channelId) {
case CHANNEL_HEATSETPOINT:
Integer newHeatsetpoint = getTemperatureFromCommand(command);
if (newHeatsetpoint == null) {
logger.warn("Heatsetpoint must not be null.");
return;
}
newConfig.heatsetpoint = newHeatsetpoint;
break;
case CHANNEL_TEMPERATURE_OFFSET:
Integer newOffset = getTemperatureFromCommand(command);
if (newOffset == null) {
logger.warn("Offset must not be null.");
return;
}
newConfig.offset = newOffset;
break;
case CHANNEL_THERMOSTAT_MODE:
if (command instanceof StringType) {
String thermostatMode = ((StringType) command).toString();
try {
newConfig.mode = ThermostatMode.valueOf(thermostatMode);
} catch (IllegalArgumentException ex) {
logger.warn("Invalid thermostat mode: {}. Valid values: {}", thermostatMode,
ThermostatMode.values());
return;
}
if (newConfig.mode == ThermostatMode.UNKNOWN) {
logger.warn("Invalid thermostat mode: {}. Valid values: {}", thermostatMode,
ThermostatMode.values());
return;
}
} else {
return;
}
break;
default:
// no supported command
return;
}
AsyncHttpClient asyncHttpClient = http;
if (asyncHttpClient == null) {
return;
}
String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "sensors", config.id,
"config");
String json = gson.toJson(newConfig);
logger.trace("Sending {} to sensor {} via {}", json, config.id, url);
asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> {
String bodyContent = v.getBody();
logger.trace("Result code={}, body={}", v.getResponseCode(), bodyContent);
if (!bodyContent.contains("success")) {
logger.debug("Sending command {} to channel {} failed: {}", command, channelUID, bodyContent);
}
}).exceptionally(e -> {
logger.debug("Sending command {} to channel {} failed:", command, channelUID, e);
return null;
});
}
@Override
protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
super.valueUpdated(channelUID, newConfig);
String mode = newConfig.mode != null ? newConfig.mode.name() : ThermostatMode.UNKNOWN.name();
String channelID = channelUID.getId();
switch (channelID) {
case CHANNEL_HEATSETPOINT:
updateQuantityTypeChannel(channelID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100);
break;
case CHANNEL_TEMPERATURE_OFFSET:
updateQuantityTypeChannel(channelID, newConfig.offset, CELSIUS, 1.0 / 100);
break;
case CHANNEL_THERMOSTAT_MODE:
if (mode != null) {
updateState(channelUID, new StringType(mode));
}
break;
}
}
@Override
protected void valueUpdated(String channelID, SensorState newState, boolean initializing) {
super.valueUpdated(channelID, newState, initializing);
switch (channelID) {
case CHANNEL_TEMPERATURE:
updateQuantityTypeChannel(channelID, newState.temperature, CELSIUS, 1.0 / 100);
break;
case CHANNEL_VALVE_POSITION:
updateQuantityTypeChannel(channelID, newState.valve, PERCENT, 100.0 / 255);
break;
}
}
@Override
protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) {
}
@Override
protected List<String> getConfigChannels() {
return CONFIG_CHANNELS;
}
private @Nullable Integer getTemperatureFromCommand(Command command) {
BigDecimal newTemperature;
if (command instanceof DecimalType) {
newTemperature = ((DecimalType) command).toBigDecimal();
} else if (command instanceof QuantityType) {
newTemperature = ((QuantityType) command).toUnit(CELSIUS).toBigDecimal();
} else {
return null;
}
return newTemperature.scaleByPowerOfTen(2).intValue();
}
}

View File

@@ -0,0 +1,258 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.*;
import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.dto.SensorConfig;
import org.openhab.binding.deconz.internal.dto.SensorState;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* This sensor Thing doesn't establish any connections, that is done by the bridge Thing.
*
* It waits for the bridge to come online, grab the websocket connection and bridge configuration
* and registers to the websocket connection as a listener.
*
* A REST API call is made to get the initial sensor state.
*
* Every sensor and switch is supported by this Thing, because a unified state is kept
* in {@link #sensorState}. Every field that got received by the REST API for this specific
* sensor is published to the framework.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class SensorThingHandler extends SensorBaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
.unmodifiableSet(Stream.of(THING_TYPE_PRESENCE_SENSOR, THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR,
THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR, THING_TYPE_TEMPERATURE_SENSOR,
THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_PRESSURE_SENSOR, THING_TYPE_SWITCH,
THING_TYPE_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR,
THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL).collect(Collectors.toSet()));
private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
CHANNEL_TEMPERATURE);
private final Logger logger = LoggerFactory.getLogger(SensorThingHandler.class);
public SensorThingHandler(Thing thing, Gson gson) {
super(thing, gson);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (!(command instanceof RefreshType)) {
return;
}
sensorState.buttonevent = null;
valueUpdated(channelUID.getId(), sensorState, false);
}
@Override
protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
super.valueUpdated(channelUID, newConfig);
Float temperature = newConfig.temperature;
switch (channelUID.getId()) {
case CHANNEL_TEMPERATURE:
if (temperature != null) {
updateState(channelUID, new QuantityType<>(temperature / 100, CELSIUS));
}
break;
}
}
@Override
protected void valueUpdated(String channelID, SensorState newState, boolean initializing) {
super.valueUpdated(channelID, newState, initializing);
switch (channelID) {
case CHANNEL_BATTERY_LEVEL:
updateDecimalTypeChannel(channelID, newState.battery);
break;
case CHANNEL_LIGHT:
Boolean dark = newState.dark;
if (dark != null) {
Boolean daylight = newState.daylight;
if (dark) { // if it's dark, it's dark ;)
updateState(channelID, new StringType("Dark"));
} else if (daylight != null) { // if its not dark, it might be between darkness and daylight
if (daylight) {
updateState(channelID, new StringType("Daylight"));
} else {
updateState(channelID, new StringType("Sunset"));
}
} else { // if no daylight value is known, we assume !dark means daylight
updateState(channelID, new StringType("Daylight"));
}
}
break;
case CHANNEL_POWER:
updateQuantityTypeChannel(channelID, newState.power, WATT);
break;
case CHANNEL_CONSUMPTION:
updateQuantityTypeChannel(channelID, newState.consumption, WATT_HOUR);
break;
case CHANNEL_VOLTAGE:
updateQuantityTypeChannel(channelID, newState.voltage, VOLT);
break;
case CHANNEL_CURRENT:
updateQuantityTypeChannel(channelID, newState.current, MILLI(AMPERE));
break;
case CHANNEL_LIGHT_LUX:
updateQuantityTypeChannel(channelID, newState.lux, LUX);
break;
case CHANNEL_COLOR:
final double @Nullable [] xy = newState.xy;
if (xy != null && xy.length == 2) {
updateState(channelID, HSBType.fromXY((float) xy[0], (float) xy[1]));
}
break;
case CHANNEL_LIGHT_LEVEL:
updateDecimalTypeChannel(channelID, newState.lightlevel);
break;
case CHANNEL_DARK:
updateSwitchChannel(channelID, newState.dark);
break;
case CHANNEL_DAYLIGHT:
updateSwitchChannel(channelID, newState.daylight);
break;
case CHANNEL_TEMPERATURE:
updateQuantityTypeChannel(channelID, newState.temperature, CELSIUS, 1.0 / 100);
break;
case CHANNEL_HUMIDITY:
updateQuantityTypeChannel(channelID, newState.humidity, PERCENT, 1.0 / 100);
break;
case CHANNEL_PRESSURE:
updateQuantityTypeChannel(channelID, newState.pressure, HECTO(PASCAL));
break;
case CHANNEL_PRESENCE:
updateSwitchChannel(channelID, newState.presence);
break;
case CHANNEL_VALUE:
updateDecimalTypeChannel(channelID, newState.status);
break;
case CHANNEL_OPENCLOSE:
Boolean open = newState.open;
if (open != null) {
updateState(channelID, open ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
}
break;
case CHANNEL_WATERLEAKAGE:
updateSwitchChannel(channelID, newState.water);
break;
case CHANNEL_FIRE:
updateSwitchChannel(channelID, newState.fire);
break;
case CHANNEL_ALARM:
updateSwitchChannel(channelID, newState.alarm);
break;
case CHANNEL_TAMPERED:
updateSwitchChannel(channelID, newState.tampered);
break;
case CHANNEL_VIBRATION:
updateSwitchChannel(channelID, newState.vibration);
break;
case CHANNEL_CARBONMONOXIDE:
updateSwitchChannel(channelID, newState.carbonmonoxide);
break;
case CHANNEL_BUTTON:
updateDecimalTypeChannel(channelID, newState.buttonevent);
break;
case CHANNEL_BUTTONEVENT:
Integer buttonevent = newState.buttonevent;
if (buttonevent != null && !initializing) {
triggerChannel(channelID, String.valueOf(buttonevent));
}
break;
case CHANNEL_GESTURE:
updateDecimalTypeChannel(channelID, newState.gesture);
break;
case CHANNEL_GESTUREEVENT:
Integer gesture = newState.gesture;
if (gesture != null && !initializing) {
triggerChannel(channelID, String.valueOf(gesture));
}
break;
}
}
@Override
protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) {
// some Xiaomi sensors
if (sensorConfig.temperature != null) {
createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE);
}
// ZHAPresence - e.g. IKEA TRÅDFRI motion sensor
if (sensorState.dark != null) {
createChannel(CHANNEL_DARK, ChannelKind.STATE);
}
// ZHAConsumption - e.g Bitron 902010/25 or Heiman SmartPlug
if (sensorState.power != null) {
createChannel(CHANNEL_POWER, ChannelKind.STATE);
}
// ZHAPower - e.g. Heiman SmartPlug
if (sensorState.voltage != null) {
createChannel(CHANNEL_VOLTAGE, ChannelKind.STATE);
}
if (sensorState.current != null) {
createChannel(CHANNEL_CURRENT, ChannelKind.STATE);
}
// IAS Zone sensor - e.g. Heiman HS1MS motion sensor
if (sensorState.tampered != null) {
createChannel(CHANNEL_TAMPERED, ChannelKind.STATE);
}
// e.g. Aqara Cube
if (sensorState.gesture != null) {
createChannel(CHANNEL_GESTURE, ChannelKind.STATE);
createChannel(CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER);
}
}
@Override
protected List<String> getConfigChannels() {
return CONFIG_CHANNELS;
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ThingConfig} class holds the configuration properties of a sensor Thing.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ThingConfig {
public String id = "";
public int lastSeenPolling = 0;
public @Nullable Double transitiontime;
}

View File

@@ -0,0 +1,135 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.netutils;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.http.HttpMethod;
/**
* An asynchronous API for HTTP interactions.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class AsyncHttpClient {
private final HttpClient client;
public AsyncHttpClient(HttpClient client) {
this.client = client;
}
/**
* Perform a POST request
*
* @param address The address
* @param jsonString The message body
* @param timeout A timeout
* @return The result
*/
public CompletableFuture<Result> post(String address, String jsonString, int timeout) {
return doNetwork(HttpMethod.POST, address, jsonString, timeout);
}
/**
* Perform a PUT request
*
* @param address The address
* @param jsonString The message body
* @param timeout A timeout
* @return The result
*/
public CompletableFuture<Result> put(String address, String jsonString, int timeout) {
return doNetwork(HttpMethod.PUT, address, jsonString, timeout);
}
/**
* Perform a GET request
*
* @param address The address
* @param timeout A timeout
* @return The result
*/
public CompletableFuture<Result> get(String address, int timeout) {
return doNetwork(HttpMethod.GET, address, null, timeout);
}
/**
* Perform a DELETE request
*
* @param address The address
* @param timeout A timeout
* @return The result
*/
public CompletableFuture<Result> delete(String address, int timeout) {
return doNetwork(HttpMethod.DELETE, address, null, timeout);
}
private CompletableFuture<Result> doNetwork(HttpMethod method, String address, @Nullable String body, int timeout) {
final CompletableFuture<Result> f = new CompletableFuture<>();
Request request = client.newRequest(URI.create(address));
if (body != null) {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
final InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(
byteArrayInputStream)) {
request.content(inputStreamContentProvider, "application/json");
} catch (Exception e) {
f.completeExceptionally(e);
return f;
}
}
request.method(method).timeout(timeout, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
@NonNullByDefault({})
@Override
public void onComplete(org.eclipse.jetty.client.api.Result result) {
final HttpResponse response = (HttpResponse) result.getResponse();
if (result.getFailure() != null) {
f.completeExceptionally(result.getFailure());
return;
}
f.complete(new Result(getContentAsString(), response.getStatus()));
}
});
return f;
}
public static class Result {
private final String body;
private final int responseCode;
public Result(String body, int responseCode) {
this.body = body;
this.responseCode = responseCode;
}
public String getBody() {
return body;
}
public int getResponseCode() {
return responseCode;
}
}
}

View File

@@ -0,0 +1,152 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.netutils;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* Establishes and keeps a websocket connection to the deCONZ software.
*
* The connection is closed by deCONZ now and then and needs to be re-established.
*
* @author David Graeff - Initial contribution
*/
@WebSocket
@NonNullByDefault
public class WebSocketConnection {
private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private final WebSocketClient client;
private final WebSocketConnectionListener connectionListener;
private final Map<String, WebSocketMessageListener> sensorListener = new ConcurrentHashMap<>();
private final Map<String, WebSocketMessageListener> lightListener = new ConcurrentHashMap<>();
private final Gson gson;
private boolean connected = false;
public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson) {
this.connectionListener = listener;
this.client = client;
this.client.setMaxIdleTimeout(0);
this.gson = gson;
}
public void start(String ip) {
if (connected) {
return;
}
try {
URI destUri = URI.create("ws://" + ip);
client.start();
logger.debug("Connecting to: {}", destUri);
client.connect(this, destUri).get();
} catch (Exception e) {
connectionListener.connectionError(e);
}
}
public void close() {
try {
connected = false;
client.stop();
} catch (Exception e) {
logger.debug("Error while closing connection", e);
}
client.destroy();
}
public void registerSensorListener(String sensorID, WebSocketMessageListener listener) {
sensorListener.put(sensorID, listener);
}
public void unregisterSensorListener(String sensorID) {
sensorListener.remove(sensorID);
}
public void registerLightListener(String lightID, WebSocketMessageListener listener) {
lightListener.put(lightID, listener);
}
public void unregisterLightListener(String lightID) {
sensorListener.remove(lightID);
}
@OnWebSocketConnect
public void onConnect(Session session) {
connected = true;
logger.debug("Connect: {}", session.getRemoteAddress().getAddress());
connectionListener.connectionEstablished();
}
@SuppressWarnings("null")
@OnWebSocketMessage
public void onMessage(String message) {
logger.trace("Raw data received by websocket: {}", message);
DeconzBaseMessage changedMessage = gson.fromJson(message, DeconzBaseMessage.class);
switch (changedMessage.r) {
case "sensors":
WebSocketMessageListener listener = sensorListener.get(changedMessage.id);
if (listener != null) {
listener.messageReceived(changedMessage.id, gson.fromJson(message, SensorMessage.class));
} else {
logger.trace("Couldn't find sensor listener for id {}", changedMessage.id);
}
break;
case "lights":
listener = lightListener.get(changedMessage.id);
if (listener != null) {
listener.messageReceived(changedMessage.id, gson.fromJson(message, LightMessage.class));
} else {
logger.trace("Couldn't find light listener for id {}", changedMessage.id);
}
break;
default:
logger.debug("Unknown message type: {}", changedMessage.r);
}
}
@OnWebSocketError
public void onError(Throwable cause) {
connected = false;
connectionListener.connectionError(cause);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
connected = false;
connectionListener.connectionLost(reason);
}
public boolean isConnected() {
return connected;
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.netutils;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Informs about the websocket connection.
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public interface WebSocketConnectionListener {
/**
* An error occurred during connection or while connecting.
*
* @param e The error
*/
void connectionError(Throwable e);
/**
* Connection successfully established.
*/
void connectionEstablished();
/**
* Connection lost. A reconnect timer has been started.
*
* @param reason A reason for the disconnection
*/
void connectionLost(String reason);
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.netutils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
/**
* Informs about received messages
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public interface WebSocketMessageListener {
/**
* A new message was received
*
* @param sensorID The sensor ID (API endpoint)
* @param message The received message
*/
void messageReceived(String sensorID, DeconzBaseMessage message);
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.types;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Type of a light as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.LightMessage}
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public enum LightType {
ON_OFF_LIGHT("On/Off light"),
ON_OFF_PLUGIN_UNIT("On/Off plug-in unit"),
SMART_PLUG("Smart plug"),
EXTENDED_COLOR_LIGHT("Extended color light"),
COLOR_LIGHT("Color light"),
COLOR_DIMMABLE_LIGHT("Color dimmable light"),
COLOR_TEMPERATURE_LIGHT("Color temperature light"),
DIMMABLE_LIGHT("Dimmable light"),
DIMMABLE_PLUGIN_UNIT("Dimmable plug-in unit"),
WINDOW_COVERING_DEVICE("Window covering device"),
CONFIGURATION_TOOL("Configuration tool"),
WARNING_DEVICE("Warning device"),
UNKNOWN("");
private static final Map<String, LightType> MAPPING = Arrays.stream(LightType.values())
.collect(Collectors.toMap(v -> v.type, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(LightType.class);
private String type;
LightType(String type) {
this.type = type;
}
public static LightType fromString(String s) {
LightType lightType = MAPPING.getOrDefault(s, UNKNOWN);
if (lightType == UNKNOWN) {
LOGGER.debug("Unknown light type '{}' found. This should be reported.", s);
}
return lightType;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.types;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Custom deserializer for {@link LightType}
*
* @author Jan N. Klug - Initial contribution
*/
public class LightTypeDeserializer implements JsonDeserializer<LightType> {
@Override
public LightType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
String s = json.getAsString();
return s == null ? LightType.UNKNOWN : LightType.fromString(s);
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.types;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Thermostat mode as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.SensorConfig}
*
* @author Lukas Agethen - Initial contribution
*/
@NonNullByDefault
public enum ThermostatMode {
AUTO("auto"),
HEAT("heat"),
OFF("off"),
UNKNOWN("");
private static final Map<String, ThermostatMode> MAPPING = Arrays.stream(ThermostatMode.values())
.collect(Collectors.toMap(v -> v.deconzValue, v -> v));
private static final Logger LOGGER = LoggerFactory.getLogger(ThermostatMode.class);
private String deconzValue;
ThermostatMode(String deconzValue) {
this.deconzValue = deconzValue;
}
public String getDeconzValue() {
return deconzValue;
}
public static ThermostatMode fromString(String s) {
ThermostatMode thermostatMode = MAPPING.getOrDefault(s, UNKNOWN);
if (thermostatMode == UNKNOWN) {
LOGGER.debug("Unknown thermostat mode '{}' found. This should be reported.", s);
}
return thermostatMode;
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal.types;
import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
* Custom (de)serializer for {@link ThermostatMode}
*
* @author Lukas Agethen - Initial contribution
*/
@NonNullByDefault
public class ThermostatModeGsonTypeAdapter implements JsonDeserializer<ThermostatMode>, JsonSerializer<ThermostatMode> {
@Override
public ThermostatMode deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
@Nullable JsonDeserializationContext context) throws JsonParseException {
JsonElement jsonLocal = json;
if (jsonLocal != null) {
String s = jsonLocal.getAsString();
return s == null ? ThermostatMode.UNKNOWN : ThermostatMode.fromString(s);
}
return ThermostatMode.UNKNOWN;
}
@Override
public JsonElement serialize(ThermostatMode src, @Nullable Type typeOfSrc,
@Nullable JsonSerializationContext context) throws JsonParseException {
return src != ThermostatMode.UNKNOWN ? new JsonPrimitive(src.getDeconzValue()) : JsonNull.INSTANCE;
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="deconz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Dresden Elektronik deCONZ Binding</name>
<description>Allows to use the real-time channel of the deCONZ software for Zigbee sensors and switches. deCONZ is the
accompanying software for the Raspbee and Conbee Zigbee dongles from Dresden Elektronik. Is meant to be used together
with the HUE binding which makes the lights and plugs available.</description>
<author>David Graeff</author>
</binding:binding>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:deconz:bridge">
<parameter name="host" type="text" required="true">
<label>Host Address</label>
<context>network-address</context>
<description>IP address or host name of deCONZ interface.</description>
</parameter>
<parameter name="httpPort" type="integer" required="false" min="1" max="65535">
<label>HTTP Port</label>
<description>Port of the deCONZ HTTP interface.</description>
<default>80</default>
</parameter>
<parameter name="port" type="integer" required="false" min="1" max="65535">
<label>Websocket Port</label>
<description>Port of the deCONZ Websocket.</description>
<advanced>true</advanced>
</parameter>
<parameter name="apikey" type="text" required="false">
<label>API Key</label>
<context>password</context>
<description>If no API Key is provided, a new one will be requested. You need to authorize the access on the deCONZ
web interface.</description>
</parameter>
<parameter name="timeout" type="integer" required="false" unit="ms" min="0">
<label>Timeout</label>
<description>Timeout for asynchronous HTTP requests (in milliseconds).</description>
<advanced>true</advanced>
<default>2000</default>
</parameter>
</config-description>
<config-description uri="thing-type:deconz:sensor">
<parameter name="id" type="text" required="true">
<label>Device ID</label>
<description>The deCONZ bridge assigns an integer number ID to each device.</description>
</parameter>
<parameter name="lastSeenPolling" type="integer" min="0" unit="min">
<label>LastSeen Poll Interval</label>
<description>Interval to poll the deCONZ Gateway for this sensor's "lastSeen" channel. Polling is disabled when set
to 0.</description>
<default>0</default>
</parameter>
</config-description>
<config-description uri="thing-type:deconz:light">
<parameter name="id" type="text" required="true">
<label>Device ID</label>
<description>The deCONZ bridge assigns an integer number ID to each device.</description>
</parameter>
<parameter name="transitiontime" type="decimal" required="false" min="0" unit="s">
<label>Transition Time</label>
<description>Time to move between two states. If empty, the default of the device is used. Resolution is 1/10 second.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,4 @@
# binding
binding.deconz.name = Dresden Elektronik deCONZ Binding
binding.deconz.description = Unterstützt die Raspbee und Conbee Zigbee Dongles via deCONZ

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="deconz"
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="warningdevice">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Warning Device</label>
<description>A warning device</description>
<channels>
<channel id="alert" typeId="alert"></channel>
</channels>
</thing-type>
<thing-type id="windowcovering">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Window Covering</label>
<description>A device to cover windows.</description>
<channels>
<channel typeId="position" id="position"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<thing-type id="onofflight">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>On/Off Light</label>
<description>A light that can be turned on or off.</description>
<channels>
<channel typeId="onoff" id="switch"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<thing-type id="dimmablelight">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Dimmable Light</label>
<description>A dimmable light.</description>
<channels>
<channel typeId="brightness" id="brightness"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/>
</thing-type>
<thing-type id="colortemperaturelight">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Color-Temperature Light</label>
<description>A dimmable light with adjustable color temperature.</description>
<channels>
<channel typeId="brightness" id="brightness"/>
<channel typeId="ct" id="color_temperature"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/>
</thing-type>
<thing-type id="colorlight">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Color Light</label>
<description>A dimmable light with adjustable color.</description>
<channels>
<channel typeId="color" id="color"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/>
</thing-type>
<thing-type id="extendedcolorlight">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Color Light</label>
<description>A dimmable light with adjustable color.</description>
<channels>
<channel typeId="color" id="color"/>
<channel typeId="ct" id="color_temperature"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:light"/>
</thing-type>
<channel-type id="position">
<item-type>Rollershutter</item-type>
<label>Position</label>
<state pattern="%.1f %%"/>
</channel-type>
<channel-type id="onoff">
<item-type>Switch</item-type>
<label>On/Off</label>
</channel-type>
<channel-type id="brightness">
<item-type>Dimmer</item-type>
<label>Brightness</label>
<state pattern="%.1f %%"/>
</channel-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>Color</label>
</channel-type>
<channel-type id="ct">
<item-type>Number</item-type>
<label>Color Temperature</label>
<state pattern="%d" min="15" max="100000" step="100"/>
</channel-type>
<channel-type id="alert">
<item-type>Switch</item-type>
<label>Alert</label>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,559 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="deconz"
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="presencesensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Presence Sensor</label>
<description>A Presence sensor</description>
<channels>
<channel typeId="presence" id="presence"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="presence">
<item-type>Switch</item-type>
<label>Presence</label>
<description>Presence detected</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="last_updated">
<item-type>DateTime</item-type>
<label>Last Updated</label>
<description>The date and time when the sensor was last updated.</description>
<category>Time</category>
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
</channel-type>
<channel-type id="last_seen">
<item-type>DateTime</item-type>
<label>Last Seen</label>
<description>The date and time when the sensor was last seen.</description>
<category>Time</category>
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
</channel-type>
<thing-type id="powersensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Power Sensor</label>
<description>A power sensor</description>
<channels>
<channel typeId="power" id="power"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="power">
<item-type>Number:Power</item-type>
<label>Power</label>
<description>Current power usage</description>
<category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
<channel-type id="voltage">
<item-type>Number:ElectricPotential</item-type>
<label>Voltage</label>
<description>Current voltage</description>
<category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="current">
<item-type>Number:ElectricCurrent</item-type>
<label>Current</label>
<description>Current current</description>
<category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<thing-type id="consumptionsensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Consumption Sensor</label>
<description>A consumption sensor</description>
<channels>
<channel typeId="consumption" id="consumption"></channel>
<channel typeId="last_updated" id="last_updated"></channel>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="consumption">
<item-type>Number:Energy</item-type>
<label>Consumption</label>
<description>Current consumption</description>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
<thing-type id="colorcontrol">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Color Controller</label>
<channels>
<channel typeId="color" id="color"/>
<channel typeId="buttonevent" id="buttonevent"/>
<channel typeId="button" id="button"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="color">
<item-type>Color</item-type>
<label>Color</label>
<description>Allows to control a color</description>
<state readOnly="true"></state>
</channel-type>
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Switch/Button</label>
<description>A switch or button</description>
<channels>
<channel typeId="buttonevent" id="buttonevent"/>
<channel typeId="button" id="button"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="buttonevent">
<kind>Trigger</kind>
<label>Button Trigger</label>
<description>This channel is triggered on a button event. The trigger payload consists of the button event number.
</description>
<event></event>
</channel-type>
<channel-type id="button">
<item-type>Number</item-type>
<label>Button</label>
<description>The Button that was last pressed on the switch.</description>
<state readOnly="true" pattern="%d"></state>
</channel-type>
<channel-type id="gestureevent">
<kind>Trigger</kind>
<label>Gesture Trigger</label>
<description>This channel is triggered on a gesture event. The trigger payload consists of the gesture event number.</description>
<event></event>
</channel-type>
<channel-type id="gesture">
<item-type>Number</item-type>
<label>Gesture</label>
<description>A gesture that was performed with the switch.</description>
<state readOnly="true" pattern="%d">
<options>
<option value="0">None</option>
<option value="1">Shake</option>
<option value="2">Drop</option>
<option value="3">Flip 90</option>
<option value="4">Flip 180</option>
<option value="5">Push</option>
<option value="6">Double Tap</option>
<option value="7">Rotate Clockwise</option>
<option value="8">Rotate Counter Clockwise</option>
</options>
</state>
</channel-type>
<thing-type id="lightsensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Light Sensor</label>
<description>A light sensor</description>
<channels>
<channel typeId="lightlux" id="lightlux"/>
<channel typeId="light_level" id="light_level"/>
<channel typeId="dark" id="dark"/>
<channel typeId="daylight" id="daylight"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="lightlux">
<item-type>Number:Illuminance</item-type>
<label>Illuminance</label>
<description>Current light illuminance</description>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
<channel-type id="light_level" advanced="true">
<item-type>Number</item-type>
<label>Light Level</label>
<description>Current light level.</description>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="dark">
<item-type>Switch</item-type>
<label>Dark</label>
<description>Light level is below the darkness threshold.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="daylight">
<item-type>Switch</item-type>
<label>Daylight</label>
<description>Light level is above the daylight threshold.</description>
<state readOnly="true"/>
</channel-type>
<thing-type id="temperaturesensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Temperature Sensor</label>
<description>A temperature sensor</description>
<channels>
<channel typeId="temperature" id="temperature"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current temperature</description>
<state readOnly="true" pattern="%.2f %unit%"></state>
</channel-type>
<thing-type id="humiditysensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Humidity Sensor</label>
<description>A humidity sensor</description>
<channels>
<channel typeId="humidity" id="humidity"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Current humidity</description>
<state readOnly="true" pattern="%.2f %unit%"></state>
</channel-type>
<thing-type id="pressuresensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Pressure Sensor</label>
<description>A pressure senor</description>
<channels>
<channel typeId="pressure" id="pressure"></channel>
<channel typeId="last_updated" id="last_updated"></channel>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="pressure">
<item-type>Number:Pressure</item-type>
<label>Pressure</label>
<description>Current pressure</description>
<state readOnly="true" pattern="%.1f %unit%"></state>
</channel-type>
<thing-type id="daylightsensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Daylight Sensor</label>
<description>A daylight sensor</description>
<channels>
<channel typeId="value" id="value"></channel>
<channel typeId="light" id="light"></channel>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="value">
<item-type>Number</item-type>
<label>Daylight Value</label>
<description>Dawn is around 130, sunrise at 140, sunset at 190, and dusk at 210</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="light">
<item-type>String</item-type>
<label>Lightlevel</label>
<description>A light level</description>
<state readOnly="true">
<options>
<option value="daylight">Daylight</option>
<option value="sunset">Sunset</option>
<option value="dark">Dark</option>
</options>
</state>
</channel-type>
<thing-type id="openclosesensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Open/Close Sensor</label>
<description>A open/close sensor</description>
<channels>
<channel typeId="open" id="open"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="open">
<item-type>Contact</item-type>
<label>Open/Close</label>
<description>Open/Close detected</description>
<state readOnly="true"></state>
</channel-type>
<thing-type id="waterleakagesensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Water Leakage Sensor</label>
<description>A water leakage sensor</description>
<channels>
<channel typeId="waterleakage" id="waterleakage"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="waterleakage">
<item-type>Switch</item-type>
<label>Water Leakage</label>
<description>Water leakage detected</description>
<state readOnly="true"/>
</channel-type>
<thing-type id="firesensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Fire Sensor</label>
<description>A fire sensor</description>
<channels>
<channel typeId="fire" id="fire"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="fire">
<item-type>Switch</item-type>
<label>Fire</label>
<description>A fire was detected.</description>
<state readOnly="true"/>
</channel-type>
<thing-type id="alarmsensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Alarm Sensor</label>
<description>An alarm sensor</description>
<channels>
<channel typeId="alarm" id="alarm"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="alarm">
<item-type>Switch</item-type>
<label>Alarm</label>
<description>Alarm was triggered.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="tampered">
<item-type>Switch</item-type>
<label>Tampered</label>
<description>A zone is being tampered.</description>
<state readOnly="true"/>
</channel-type>
<thing-type id="vibrationsensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Vibration Sensor</label>
<description>A vibration sensor</description>
<channels>
<channel typeId="vibration" id="vibration"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="vibration">
<item-type>Switch</item-type>
<label>Vibration</label>
<description>Vibration was detected.</description>
<state readOnly="true"/>
</channel-type>
<thing-type id="batterysensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Battery Sensor</label>
<description>A battery sensor</description>
<channels>
<channel typeId="battery" id="battery_level"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="battery">
<item-type>Number</item-type>
<label>Battery</label>
<description>The battery state.</description>
<state pattern="%d %%" readOnly="true"/>
</channel-type>
<thing-type id="carbonmonoxidesensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Carbon-monoxide Sensor</label>
<channels>
<channel typeId="carbonmonoxide" id="carbonmonoxide"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="carbonmonoxide">
<item-type>Switch</item-type>
<label>Carbon-monoxide</label>
<description>Carbon-monoxide was detected.</description>
<state readOnly="true"/>
</channel-type>
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Thermostat</label>
<description>A Thermostat sensor/actor</description>
<channels>
<channel typeId="temperature" id="temperature"/>
<channel typeId="heatsetpoint" id="heatsetpoint"/>
<channel typeId="mode" id="mode"/>
<channel typeId="offset" id="offset"/>
<channel typeId="valve" id="valve"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="heatsetpoint">
<item-type>Number:Temperature</item-type>
<label>Target temperature</label>
<description>Target temperature</description>
<state pattern="%.1f %unit%" step="0.5" max="28" min="6"></state>
</channel-type>
<channel-type id="mode">
<item-type>String</item-type>
<label>Mode</label>
<description>Current mode</description>
<state>
<options>
<option value="AUTO">auto</option>
<option value="HEAT">heat</option>
<option value="OFF">off</option>
</options>
</state>
</channel-type>
<channel-type id="offset">
<item-type>Number:Temperature</item-type>
<label>Offset</label>
<description>Temperature offset</description>
<state pattern="%.2f %unit%" step="0.01"></state>
</channel-type>
<channel-type id="valve">
<item-type>Number:Dimensionless</item-type>
<label>Valve position</label>
<description>Current valve position</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="deconz"
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">
<bridge-type id="deconz">
<label>deCONZ</label>
<description>A running deCONZ software instance</description>
<config-description-ref uri="thing-type:deconz:bridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2020 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.deconz;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.apache.commons.io.IOUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService;
import org.openhab.binding.deconz.internal.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* This class provides tests for deconz binding
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class DeconzTest {
private @NonNullByDefault({}) Gson gson;
@Mock
private @NonNullByDefault({}) DiscoveryListener discoveryListener;
@Mock
private @NonNullByDefault({}) DeconzBridgeHandler bridgeHandler;
@Mock
private @NonNullByDefault({}) Bridge bridge;
@Before
public void initialize() {
initMocks(this);
Mockito.doAnswer(answer -> bridge).when(bridgeHandler).getThing();
Mockito.doAnswer(answer -> new ThingUID("deconz", "mybridge")).when(bridge).getUID();
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
@Test
public void discoveryTest() throws IOException {
BridgeFullState bridgeFullState = getObjectFromJson("discovery.json", BridgeFullState.class, gson);
Assert.assertNotNull(bridgeFullState);
Assert.assertEquals(6, bridgeFullState.lights.size());
Assert.assertEquals(9, bridgeFullState.sensors.size());
ThingDiscoveryService discoveryService = new ThingDiscoveryService();
discoveryService.setThingHandler(bridgeHandler);
discoveryService.addDiscoveryListener(discoveryListener);
discoveryService.stateRequestFinished(bridgeFullState);
Mockito.verify(discoveryListener, times(15)).thingDiscovered(any(), any());
}
public static <T> T getObjectFromJson(String filename, Class<T> clazz, Gson gson) throws IOException {
String json = IOUtils.toString(DeconzTest.class.getResourceAsStream(filename), StandardCharsets.UTF_8.name());
return gson.fromJson(json, clazz);
}
@Test
public void dateTimeConversionTest() {
DateTimeType dateTime = Util.convertTimestampToDateTime("2020-08-22T11:09Z");
Assert.assertEquals(new DateTimeType(ZonedDateTime.parse("2020-08-22T11:09:00Z")), dateTime);
dateTime = Util.convertTimestampToDateTime("2020-08-22T11:09:47");
Assert.assertEquals(
new DateTimeType(ZonedDateTime.parse("2020-08-22T11:09:47Z")).toZone(ZoneId.systemDefault()), dateTime);
}
}

View File

@@ -0,0 +1,189 @@
/**
* Copyright (c) 2010-2020 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.deconz;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
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.binding.builder.ThingBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* This class provides tests for deconz lights
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class LightsTest {
private @NonNullByDefault({}) Gson gson;
@Mock
private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
@Mock
private @NonNullByDefault({}) StateDescriptionProvider stateDescriptionProvider;
@Before
public void initialize() {
initMocks(this);
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
@Test
public void colorTemperatureLightUpdateTest() throws IOException {
LightMessage lightMessage = DeconzTest.getObjectFromJson("colortemperature.json", LightMessage.class, gson);
Assert.assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("21")));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500")));
}
@Test
public void colorTemperatureLightStateDescriptionProviderTest() {
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);
Map<String, String> properties = new HashMap<>();
properties.put(PROPERTY_CT_MAX, "500");
properties.put(PROPERTY_CT_MIN, "200");
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider) {
// avoid warning when initializing
@Override
public @Nullable Bridge getBridge() {
return null;
}
};
lightThingHandler.initialize();
Mockito.verify(stateDescriptionProvider).setDescription(eq(channelUID_ct), any());
}
@Test
public void dimmableLightUpdateTest() throws IOException {
LightMessage lightMessage = DeconzTest.getObjectFromJson("dimmable.json", LightMessage.class, gson);
Assert.assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("38")));
}
@Test
public void dimmableLightOverrangeUpdateTest() throws IOException {
LightMessage lightMessage = DeconzTest.getObjectFromJson("dimmable_overrange.json", LightMessage.class, gson);
Assert.assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("100")));
}
@Test
public void dimmableLightUnderrangeUpdateTest() throws IOException {
LightMessage lightMessage = DeconzTest.getObjectFromJson("dimmable_underrange.json", LightMessage.class, gson);
Assert.assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("0")));
}
@Test
public void windowCoveringUpdateTest() throws IOException {
LightMessage lightMessage = DeconzTest.getObjectFromJson("windowcovering.json", LightMessage.class, gson);
Assert.assertNotNull(lightMessage);
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_pos = new ChannelUID(thingUID, CHANNEL_POSITION);
Thing light = ThingBuilder.create(THING_TYPE_WINDOW_COVERING, thingUID)
.withChannel(ChannelBuilder.create(channelUID_pos, "Rollershutter").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);
lightThingHandler.messageReceived("", lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_pos), eq(new PercentType("41")));
}
}

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2020 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.deconz;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler;
import org.openhab.binding.deconz.internal.handler.SensorThingHandler;
import org.openhab.binding.deconz.internal.types.LightType;
import org.openhab.binding.deconz.internal.types.LightTypeDeserializer;
import org.openhab.binding.deconz.internal.types.ThermostatMode;
import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter;
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.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
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.binding.builder.ThingBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* This class provides tests for deconz sensors
*
* @author Jan N. Klug - Initial contribution
* @author Lukas Agethen - Added Thermostat
*/
@NonNullByDefault
public class SensorsTest {
private @NonNullByDefault({}) Gson gson;
@Mock
private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
@Before
public void initialize() {
initMocks(this);
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter());
gson = gsonBuilder.create();
}
@Test
public void carbonmonoxideSensorUpdateTest() throws IOException {
SensorMessage sensorMessage = DeconzTest.getObjectFromJson("carbonmonoxide.json", SensorMessage.class, gson);
Assert.assertNotNull(sensorMessage);
ThingUID thingUID = new ThingUID("deconz", "sensor");
ChannelUID channelUID = new ChannelUID(thingUID, "carbonmonoxide");
Thing sensor = ThingBuilder.create(THING_TYPE_CARBONMONOXIDE_SENSOR, thingUID)
.withChannel(ChannelBuilder.create(channelUID, "Switch").build()).build();
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback);
sensorThingHandler.messageReceived("", sensorMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON));
}
@Test
public void thermostatSensorUpdateTest() throws IOException {
SensorMessage sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson);
Assert.assertNotNull(sensorMessage);
ThingUID thingUID = new ThingUID("deconz", "sensor");
ChannelUID channelValveUID = new ChannelUID(thingUID, "valve");
ChannelUID channelHeatSetPointUID = new ChannelUID(thingUID, "heatsetpoint");
ChannelUID channelModeUID = new ChannelUID(thingUID, "mode");
ChannelUID channelTemperatureUID = new ChannelUID(thingUID, "temperature");
Thing sensor = ThingBuilder.create(THING_TYPE_THERMOSTAT, thingUID)
.withChannel(ChannelBuilder.create(channelValveUID, "Number").build())
.withChannel(ChannelBuilder.create(channelHeatSetPointUID, "Number").build())
.withChannel(ChannelBuilder.create(channelModeUID, "String").build())
.withChannel(ChannelBuilder.create(channelTemperatureUID, "Number").build()).build();
SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback);
sensorThingHandler.messageReceived("", sensorMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID),
eq(new QuantityType<>(100.0, SmartHomeUnits.PERCENT)));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID),
eq(new QuantityType<>(25, SIUnits.CELSIUS)));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID),
eq(new StringType(ThermostatMode.AUTO.name())));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID),
eq(new QuantityType<>(16.5, SIUnits.CELSIUS)));
}
}

View File

@@ -0,0 +1,21 @@
{
"config": {
"battery": 100,
"on": true,
"reachable": true
},
"ep": 1,
"etag": "1902d2cdb9d256624398f9ec3d35481d",
"manufacturername": "Heiman",
"modelid": "COSensor-N",
"name": "CO Sensor",
"state": {
"carbonmonoxide": true,
"lastupdated": "none",
"lowbattery": null,
"tampered": null
},
"swversion": "2018.4.22",
"type": "ZHACarbonMonoxide",
"uniqueid": "00:28:9f:00:03:eb:80:2a-01-0500"
}

View File

@@ -0,0 +1,15 @@
{
"e": "changed",
"id": "3",
"r": "lights",
"state": {
"alert": null,
"bri": 51,
"colormode": "ct",
"ct": 400,
"on": true,
"reachable": true
},
"t": "event",
"uniqueid": "00:0b:57:ff:fe:eb:2f:84-01"
}

View File

@@ -0,0 +1,13 @@
{
"e": "changed",
"id": "4",
"r": "lights",
"state": {
"alert": null,
"bri": 96,
"on": true,
"reachable": true
},
"t": "event",
"uniqueid": "00:0b:57:ff:fe:c5:34:c4-01"
}

View File

@@ -0,0 +1,13 @@
{
"e": "changed",
"id": "4",
"r": "lights",
"state": {
"alert": null,
"bri": 270,
"on": true,
"reachable": true
},
"t": "event",
"uniqueid": "00:0b:57:ff:fe:c5:34:c4-01"
}

View File

@@ -0,0 +1,13 @@
{
"e": "changed",
"id": "4",
"r": "lights",
"state": {
"alert": null,
"bri": -1,
"on": true,
"reachable": true
},
"t": "event",
"uniqueid": "00:0b:57:ff:fe:c5:34:c4-01"
}

View File

@@ -0,0 +1,27 @@
{
"config": {
"battery": 85,
"displayflipped": null,
"heatsetpoint": 2500,
"locked": null,
"mode": "auto",
"offset": 0,
"on": true,
"reachable": true
},
"ep": 1,
"etag": "717549a99371f3ea1a5f0b40f1537094",
"lastseen": "2020-05-31T20:24:55.819",
"manufacturername": "Eurotronic",
"modelid": "SPZB0001",
"name": "Test Thermostat",
"state": {
"lastupdated": "2020-05-31T20:24:55.819",
"on": true,
"temperature": 1650,
"valve": 255
},
"swversion": "20191014",
"type": "ZHAThermostat",
"uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201"
}

View File

@@ -0,0 +1,13 @@
{
"e": "changed",
"id": "6",
"r": "lights",
"state": {
"alert": null,
"bri": 102,
"on": true,
"reachable": true
},
"t": "event",
"uniqueid": "68:0a:e2:ff:fe:6e:95:af-01"
}