added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
+ '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+ '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user