added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.milight-${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-milight" description="Milight Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.milight/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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.milight.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link MilightBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "milight";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID BRIDGEV3_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridgeV3");
|
||||
public static final ThingTypeUID BRIDGEV6_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridgeV6");
|
||||
|
||||
public static final ThingTypeUID RGB_THING_TYPE = new ThingTypeUID(BINDING_ID, "rgbLed");
|
||||
public static final ThingTypeUID RGB_V2_THING_TYPE = new ThingTypeUID(BINDING_ID, "rgbv2Led");
|
||||
public static final ThingTypeUID RGB_IBOX_THING_TYPE = new ThingTypeUID(BINDING_ID, "rgbiboxLed");
|
||||
public static final ThingTypeUID RGB_CW_WW_THING_TYPE = new ThingTypeUID(BINDING_ID, "rgbwwLed");
|
||||
public static final ThingTypeUID RGB_W_THING_TYPE = new ThingTypeUID(BINDING_ID, "rgbwLed");
|
||||
public static final ThingTypeUID WHITE_THING_TYPE = new ThingTypeUID(BINDING_ID, "whiteLed");
|
||||
|
||||
public static final Set<ThingTypeUID> BRIDGE_THING_TYPES_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(BRIDGEV3_THING_TYPE, BRIDGEV6_THING_TYPE).collect(Collectors.toSet()));
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_COLOR = "ledcolor";
|
||||
public static final String CHANNEL_NIGHTMODE = "lednightmode";
|
||||
public static final String CHANNEL_WHITEMODE = "ledwhitemode";
|
||||
public static final String CHANNEL_BRIGHTNESS = "ledbrightness";
|
||||
public static final String CHANNEL_SATURATION = "ledsaturation";
|
||||
public static final String CHANNEL_TEMP = "ledtemperature";
|
||||
public static final String CHANNEL_SPEED_REL = "animation_speed_relative";
|
||||
public static final String CHANNEL_ANIMATION_MODE = "animation_mode";
|
||||
public static final String CHANNEL_ANIMATION_MODE_REL = "animation_mode_relative";
|
||||
public static final String CHANNEL_LINKLED = "ledlink";
|
||||
public static final String CHANNEL_UNLINKLED = "ledunlink";
|
||||
|
||||
public static final int PORT_DISCOVER = 48899;
|
||||
public static final int PORT_VER3 = 8899;
|
||||
public static final int PORT_VER6 = 5987;
|
||||
|
||||
public static final String PROPERTY_SESSIONID = "sessionid";
|
||||
public static final String PROPERTY_SESSIONCONFIRMED = "sessionid_last_refresh";
|
||||
|
||||
public static final byte[] DISCOVER_MSG_V3 = "Link_Wi-Fi".getBytes();
|
||||
public static final byte[] DISCOVER_MSG_V6 = "HF-A11ASSISTHREAD".getBytes();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.milight.internal;
|
||||
|
||||
import static org.openhab.binding.milight.internal.MilightBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
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.milight.internal.handler.BridgeV3Handler;
|
||||
import org.openhab.binding.milight.internal.handler.BridgeV6Handler;
|
||||
import org.openhab.binding.milight.internal.handler.MilightV2RGBHandler;
|
||||
import org.openhab.binding.milight.internal.handler.MilightV3RGBWHandler;
|
||||
import org.openhab.binding.milight.internal.handler.MilightV3WhiteHandler;
|
||||
import org.openhab.binding.milight.internal.handler.MilightV6RGBCWWWHandler;
|
||||
import org.openhab.binding.milight.internal.handler.MilightV6RGBIBOXHandler;
|
||||
import org.openhab.binding.milight.internal.handler.MilightV6RGBWHandler;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
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.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link MilightHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.milight")
|
||||
public class MilightHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(BRIDGEV3_THING_TYPE, BRIDGEV6_THING_TYPE, RGB_V2_THING_TYPE, RGB_THING_TYPE, WHITE_THING_TYPE,
|
||||
RGB_W_THING_TYPE, RGB_CW_WW_THING_TYPE, RGB_IBOX_THING_TYPE).collect(Collectors.toSet()));
|
||||
|
||||
// The UDP queue for bridge communication is a single instance for all bridges.
|
||||
// This is because all bridges use the same radio frequency, and if multiple
|
||||
// bridges would send a command at the same time, they would interfere with
|
||||
// each other (user report!).
|
||||
private @Nullable QueuedSend queuedSend;
|
||||
|
||||
private int bridgeOffset;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
final QueuedSend queuedSend = this.queuedSend;
|
||||
if (queuedSend == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (thingTypeUID.equals(BRIDGEV3_THING_TYPE)) {
|
||||
return new BridgeV3Handler((Bridge) thing, bridgeOffset += 100);
|
||||
} else if (thingTypeUID.equals(BRIDGEV6_THING_TYPE)) {
|
||||
return new BridgeV6Handler((Bridge) thing, bridgeOffset += 100);
|
||||
} else if (thing.getThingTypeUID().equals(MilightBindingConstants.RGB_IBOX_THING_TYPE)) {
|
||||
return new MilightV6RGBIBOXHandler(thing, queuedSend);
|
||||
} else if (thing.getThingTypeUID().equals(MilightBindingConstants.RGB_CW_WW_THING_TYPE)) {
|
||||
return new MilightV6RGBCWWWHandler(thing, queuedSend);
|
||||
} else if (thing.getThingTypeUID().equals(MilightBindingConstants.RGB_W_THING_TYPE)) {
|
||||
return new MilightV6RGBWHandler(thing, queuedSend);
|
||||
} else if (thing.getThingTypeUID().equals(MilightBindingConstants.RGB_V2_THING_TYPE)) {
|
||||
return new MilightV2RGBHandler(thing, queuedSend);
|
||||
} else if (thing.getThingTypeUID().equals(MilightBindingConstants.WHITE_THING_TYPE)) {
|
||||
return new MilightV3WhiteHandler(thing, queuedSend);
|
||||
} else if (thing.getThingTypeUID().equals(MilightBindingConstants.RGB_THING_TYPE)) {
|
||||
return new MilightV3RGBWHandler(thing, queuedSend);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate(ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
queuedSend = new QueuedSend();
|
||||
queuedSend.start();
|
||||
bridgeOffset = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate(ComponentContext componentContext) {
|
||||
QueuedSend queuedSend = this.queuedSend;
|
||||
if (queuedSend != null) {
|
||||
try {
|
||||
queuedSend.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
super.deactivate(componentContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.milight.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Contains a led bulb state including the HSB value, white color temperature and animation values.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightThingState {
|
||||
public int animationMode;
|
||||
public int colorTemperature; // only for led bulbs which include white leds applicable
|
||||
public int brightness;
|
||||
public int hue360; // only for rgb(w) leds applicable (v3+)
|
||||
public int saturation; // only for rgbww leds applicable (v6+)
|
||||
|
||||
public MilightThingState() {
|
||||
reset();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
animationMode = 0;
|
||||
colorTemperature = 100; // only for led bulbs which include white leds applicable
|
||||
brightness = 100;
|
||||
hue360 = 180; // only for rgb(w) leds applicable (v3+)
|
||||
saturation = 100; // only for rgbww leds applicable (v6+)
|
||||
}
|
||||
}
|
||||
@@ -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.milight.internal.discovery;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Result callback interface.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface DiscoveryResultListener {
|
||||
/**
|
||||
* A Milight bridge got detected
|
||||
*
|
||||
* @param addr The IP address
|
||||
* @param id The bridge ID
|
||||
* @param version The bridge version (either 3 or 6)
|
||||
*/
|
||||
void bridgeDetected(InetAddress addr, String id, int version);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* 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.milight.internal.discovery;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.openhab.binding.milight.internal.handler.BridgeHandlerConfig;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager.ISessionState;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager.SessionState;
|
||||
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.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Milight bridges v3/v4/v5 and v6 can be discovered by sending specially formated UDP packets.
|
||||
* This class sends UDP packets on port PORT_DISCOVER up to three times in a row
|
||||
* and listens for the response and will call discoverResult.bridgeDetected() eventually.
|
||||
*
|
||||
* The response of the bridges is unfortunately very generic and is the unmodified response of
|
||||
* any HF-LPB100 wifi chipset. Therefore other devices as the Orvibo Smart Plugs are recognised
|
||||
* as Milight Bridges as well. For v5/v6 there are some additional checks to make sure we are
|
||||
* talking to a Milight.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.milight")
|
||||
public class MilightBridgeDiscovery extends AbstractDiscoveryService implements Runnable {
|
||||
private final Logger logger = LoggerFactory.getLogger(MilightBridgeDiscovery.class);
|
||||
|
||||
///// Static configuration
|
||||
private static final boolean ENABLE_V3 = true;
|
||||
private static final boolean ENABLE_V6 = true;
|
||||
|
||||
private @Nullable ScheduledFuture<?> backgroundFuture;
|
||||
|
||||
///// Network
|
||||
private final int receivePort;
|
||||
private final DatagramPacket discoverPacketV3;
|
||||
private final DatagramPacket discoverPacketV6;
|
||||
private boolean willbeclosed = false;
|
||||
@NonNullByDefault({})
|
||||
private DatagramSocket datagramSocket;
|
||||
private final byte[] buffer = new byte[1024];
|
||||
private final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
|
||||
///// Result and resend
|
||||
private int resendCounter = 0;
|
||||
private @Nullable ScheduledFuture<?> resendTimer;
|
||||
private final int resendTimeoutInMillis;
|
||||
private final int resendAttempts;
|
||||
|
||||
public MilightBridgeDiscovery() throws IllegalArgumentException, UnknownHostException {
|
||||
super(MilightBindingConstants.BRIDGE_THING_TYPES_UIDS, 2, true);
|
||||
this.resendAttempts = 2000 / 200;
|
||||
this.resendTimeoutInMillis = 200;
|
||||
this.receivePort = MilightBindingConstants.PORT_DISCOVER;
|
||||
discoverPacketV3 = new DatagramPacket(MilightBindingConstants.DISCOVER_MSG_V3,
|
||||
MilightBindingConstants.DISCOVER_MSG_V3.length);
|
||||
discoverPacketV6 = new DatagramPacket(MilightBindingConstants.DISCOVER_MSG_V6,
|
||||
MilightBindingConstants.DISCOVER_MSG_V6.length);
|
||||
|
||||
startDiscoveryService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
if (backgroundFuture != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
backgroundFuture = scheduler.scheduleWithFixedDelay(this::startDiscoveryService, 50, 60000 * 30,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
stopScan();
|
||||
final ScheduledFuture<?> future = backgroundFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
this.backgroundFuture = null;
|
||||
}
|
||||
stop();
|
||||
}
|
||||
|
||||
public void bridgeDetected(InetAddress addr, String id, int version) {
|
||||
ThingUID thingUID = new ThingUID(version == 6 ? MilightBindingConstants.BRIDGEV6_THING_TYPE
|
||||
: MilightBindingConstants.BRIDGEV3_THING_TYPE, id);
|
||||
|
||||
Map<String, Object> properties = new TreeMap<>();
|
||||
properties.put(BridgeHandlerConfig.CONFIG_BRIDGE_ID, id);
|
||||
properties.put(BridgeHandlerConfig.CONFIG_HOST_NAME, addr.getHostAddress());
|
||||
|
||||
String label = "Bridge " + id;
|
||||
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withLabel(label)
|
||||
.withProperties(properties).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
startDiscoveryService();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
stop();
|
||||
super.stopScan();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the scheduler to resend discover messages. Stops after a configured amount of attempts.
|
||||
*/
|
||||
private class SendDiscoverRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
// Stop after a certain amount of attempts
|
||||
if (++resendCounter > resendAttempts) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
Enumeration<NetworkInterface> e;
|
||||
try {
|
||||
e = NetworkInterface.getNetworkInterfaces();
|
||||
} catch (SocketException e1) {
|
||||
logger.error("Could not enumerate network interfaces for sending the discover packet!");
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
while (e.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = e.nextElement();
|
||||
for (InterfaceAddress address : networkInterface.getInterfaceAddresses()) {
|
||||
InetAddress broadcast = address.getBroadcast();
|
||||
if (broadcast != null && !address.getAddress().isLoopbackAddress()) {
|
||||
sendDiscover(broadcast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDiscover(InetAddress destIP) {
|
||||
if (ENABLE_V3) {
|
||||
discoverPacketV3.setAddress(destIP);
|
||||
discoverPacketV3.setPort(MilightBindingConstants.PORT_DISCOVER);
|
||||
try {
|
||||
datagramSocket.send(discoverPacketV3);
|
||||
} catch (IOException e) {
|
||||
logger.error("Sending a V3 discovery packet to {} failed. {}", destIP.getHostAddress(),
|
||||
e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (ENABLE_V6) {
|
||||
discoverPacketV6.setAddress(destIP);
|
||||
discoverPacketV6.setPort(MilightBindingConstants.PORT_DISCOVER);
|
||||
try {
|
||||
datagramSocket.send(discoverPacketV6);
|
||||
} catch (IOException e) {
|
||||
logger.error("Sending a V6 discovery packet to {} failed. {}", destIP.getHostAddress(),
|
||||
e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not stop the discovery thread (like dispose()), so discovery
|
||||
* packet responses can still be received, but will stop
|
||||
* re-sending discovery packets. Call sendDiscover() to restart sending
|
||||
* discovery packets.
|
||||
*/
|
||||
public void stop() {
|
||||
if (resendTimer != null) {
|
||||
resendTimer.cancel(false);
|
||||
resendTimer = null;
|
||||
}
|
||||
|
||||
if (willbeclosed) {
|
||||
return;
|
||||
}
|
||||
willbeclosed = true;
|
||||
datagramSocket.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a discover message and resends the message until either a valid response
|
||||
* is received or the resend counter reaches the maximum attempts.
|
||||
*
|
||||
* @param scheduler The scheduler is used for resending.
|
||||
* @throws SocketException
|
||||
*/
|
||||
public void startDiscoveryService() {
|
||||
// Do nothing if there is already a discovery running
|
||||
if (resendTimer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
willbeclosed = false;
|
||||
try {
|
||||
datagramSocket = new DatagramSocket(null);
|
||||
datagramSocket.setBroadcast(true);
|
||||
datagramSocket.setReuseAddress(true);
|
||||
datagramSocket.bind(null);
|
||||
} catch (SocketException e) {
|
||||
logger.error("Opening a socket for the milight discovery service failed. {}", e.getLocalizedMessage());
|
||||
return;
|
||||
}
|
||||
resendCounter = 0;
|
||||
resendTimer = scheduler.scheduleWithFixedDelay(new SendDiscoverRunnable(), 0, resendTimeoutInMillis,
|
||||
TimeUnit.MILLISECONDS);
|
||||
scheduler.execute(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (!willbeclosed) {
|
||||
packet.setLength(buffer.length);
|
||||
datagramSocket.receive(packet);
|
||||
// We expect packets with a format like this: 10.1.1.27,ACCF23F57AD4,HF-LPB100
|
||||
String[] msg = new String(buffer, 0, packet.getLength()).split(",");
|
||||
|
||||
if (msg.length != 2 && msg.length != 3) {
|
||||
// That data packet does not belong to a Milight bridge. Just ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// First argument is the IP
|
||||
try {
|
||||
InetAddress.getByName(msg[0]);
|
||||
} catch (UnknownHostException ignored) {
|
||||
// That data packet does not belong to a Milight bridge, we expect an IP address as first argument.
|
||||
// Just ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Second argument is the MAC address
|
||||
if (msg[1].length() != 12) {
|
||||
// That data packet does not belong to a Milight bridge, we expect a MAC address as second argument.
|
||||
// Just ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
InetAddress addressOfBridge = ((InetSocketAddress) packet.getSocketAddress()).getAddress();
|
||||
if (ENABLE_V6 && msg.length == 3) {
|
||||
if (!(msg[2].length() == 0 || "HF-LPB100".equals(msg[2]))) {
|
||||
logger.trace("Unexpected data. We expected a HF-LPB100 or empty identifier {}", msg[2]);
|
||||
continue;
|
||||
}
|
||||
if (!checkForV6Bridge(addressOfBridge, msg[1])) {
|
||||
logger.trace("The device at IP {} does not seem to be a V6 Milight bridge", msg[0]);
|
||||
continue;
|
||||
}
|
||||
bridgeDetected(addressOfBridge, msg[1], 6);
|
||||
} else if (ENABLE_V3 && msg.length == 2) {
|
||||
bridgeDetected(addressOfBridge, msg[1], 3);
|
||||
} else {
|
||||
logger.debug("Unexpected data. Expected Milight bridge message");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (willbeclosed) {
|
||||
return;
|
||||
}
|
||||
logger.warn("{}", e.getLocalizedMessage());
|
||||
} catch (InterruptedException ignore) {
|
||||
// Ignore this exception, the thread is finished now anyway
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We use the {@see MilightV6SessionManager} to establish a full session to the bridge. If we reach
|
||||
* the SESSION_VALID state within 1.3s, we can safely assume it is a V6 Milight bridge.
|
||||
*
|
||||
* @param addressOfBridge IP Address of the bridge
|
||||
* @return
|
||||
* @throws InterruptedException If waiting for the session is interrupted we throw this exception
|
||||
*/
|
||||
private boolean checkForV6Bridge(InetAddress addressOfBridge, String bridgeID) throws InterruptedException {
|
||||
Semaphore s = new Semaphore(0);
|
||||
ISessionState sessionState = (SessionState state, InetAddress address) -> {
|
||||
if (state == SessionState.SESSION_VALID) {
|
||||
s.release();
|
||||
}
|
||||
logger.debug("STATE CHANGE: {}", state);
|
||||
};
|
||||
|
||||
try (MilightV6SessionManager session = new MilightV6SessionManager(bridgeID, sessionState, addressOfBridge,
|
||||
MilightBindingConstants.PORT_VER6, MilightV6SessionManager.TIMEOUT_MS, new byte[] { 0, 0 })) {
|
||||
session.start();
|
||||
boolean success = s.tryAcquire(1, 1300, TimeUnit.MILLISECONDS);
|
||||
return success;
|
||||
} catch (IOException e) {
|
||||
logger.debug("checkForV6Bridge failed", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AbstractBridgeHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractBridgeHandler extends BaseBridgeHandler {
|
||||
protected final Logger logger = LoggerFactory.getLogger(AbstractBridgeHandler.class);
|
||||
protected volatile boolean preventReinit = false;
|
||||
protected BridgeHandlerConfig config = new BridgeHandlerConfig();
|
||||
protected @Nullable InetAddress address;
|
||||
protected @NonNullByDefault({}) DatagramSocket socket;
|
||||
protected int bridgeOffset;
|
||||
|
||||
public AbstractBridgeHandler(Bridge bridge, int bridgeOffset) {
|
||||
super(bridge);
|
||||
this.bridgeOffset = bridgeOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// There is nothing to handle in the bridge handler
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
|
||||
if (preventReinit) {
|
||||
return;
|
||||
}
|
||||
super.handleConfigurationUpdate(configurationParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection and other supportive objects.
|
||||
*
|
||||
* @param addr
|
||||
*/
|
||||
protected abstract void startConnectAndKeepAlive();
|
||||
|
||||
/**
|
||||
* You need a CONFIG_HOST_NAME and CONFIG_ID for a milight bridge handler to initialize correctly.
|
||||
* The ID is a unique 12 character long ASCII based on the bridge MAC address (for example ACCF23A20164)
|
||||
* and is send as response for a discovery message.
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(BridgeHandlerConfig.class);
|
||||
|
||||
if (!config.host.isEmpty()) {
|
||||
try {
|
||||
address = InetAddress.getByName(config.host);
|
||||
} catch (UnknownHostException ignored) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Address set, but is invalid!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
startConnectAndKeepAlive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
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.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AbstractLedHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractLedHandler extends BaseThingHandler implements LedHandlerInterface {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractLedHandler.class);
|
||||
|
||||
protected final QueuedSend sendQueue;
|
||||
/** Each bulb type including zone has to be unique. -> Each type has an offset. */
|
||||
protected final int typeOffset;
|
||||
protected final MilightThingState state = new MilightThingState();
|
||||
protected LedHandlerConfig config = new LedHandlerConfig();
|
||||
protected int port = 0;
|
||||
|
||||
protected @NonNullByDefault({}) InetAddress address;
|
||||
protected @NonNullByDefault({}) DatagramSocket socket;
|
||||
|
||||
protected int delayTimeMS = 50;
|
||||
protected int repeatTimes = 3;
|
||||
|
||||
protected int bridgeOffset;
|
||||
|
||||
/**
|
||||
* A bulb always belongs to a zone in the milight universe and we need a way to queue commands for being send.
|
||||
*
|
||||
* @param typeOffset Each bulb type including its zone has to be unique. To realise this, each type has an offset.
|
||||
* @param sendQueue The send queue.
|
||||
* @param zone A zone, usually 0 means all bulbs of the same type. [0-4]
|
||||
* @throws SocketException
|
||||
*/
|
||||
public AbstractLedHandler(Thing thing, QueuedSend sendQueue, int typeOffset) {
|
||||
super(thing);
|
||||
this.typeOffset = typeOffset;
|
||||
this.sendQueue = sendQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case MilightBindingConstants.CHANNEL_COLOR:
|
||||
updateState(channelUID, new HSBType(new DecimalType(state.hue360),
|
||||
new PercentType(state.saturation), new PercentType(state.brightness)));
|
||||
break;
|
||||
case MilightBindingConstants.CHANNEL_BRIGHTNESS:
|
||||
updateState(channelUID, new PercentType(state.brightness));
|
||||
break;
|
||||
case MilightBindingConstants.CHANNEL_SATURATION:
|
||||
updateState(channelUID, new PercentType(state.saturation));
|
||||
break;
|
||||
case MilightBindingConstants.CHANNEL_TEMP:
|
||||
updateState(channelUID, new PercentType(state.colorTemperature));
|
||||
break;
|
||||
case MilightBindingConstants.CHANNEL_ANIMATION_MODE:
|
||||
updateState(channelUID, new DecimalType(state.animationMode));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case MilightBindingConstants.CHANNEL_COLOR: {
|
||||
if (command instanceof HSBType) {
|
||||
HSBType hsb = (HSBType) command;
|
||||
this.setHSB(hsb.getHue().intValue(), hsb.getSaturation().intValue(), hsb.getBrightness().intValue(),
|
||||
state);
|
||||
updateState(MilightBindingConstants.CHANNEL_SATURATION, new PercentType(state.saturation));
|
||||
} else if (command instanceof OnOffType) {
|
||||
OnOffType hsb = (OnOffType) command;
|
||||
this.setPower(hsb == OnOffType.ON, state);
|
||||
} else if (command instanceof PercentType) {
|
||||
PercentType p = (PercentType) command;
|
||||
this.setBrightness(p.intValue(), state);
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
this.changeBrightness((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE ? 1 : -1,
|
||||
state);
|
||||
} else {
|
||||
logger.error(
|
||||
"CHANNEL_COLOR channel only supports OnOffType/IncreaseDecreaseType/HSBType/PercentType");
|
||||
}
|
||||
updateState(MilightBindingConstants.CHANNEL_BRIGHTNESS, new PercentType(state.brightness));
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_NIGHTMODE: {
|
||||
this.nightMode(state);
|
||||
updateState(channelUID, UnDefType.UNDEF);
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_WHITEMODE: {
|
||||
this.whiteMode(state);
|
||||
updateState(channelUID, UnDefType.UNDEF);
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_BRIGHTNESS: {
|
||||
if (command instanceof OnOffType) {
|
||||
OnOffType hsb = (OnOffType) command;
|
||||
this.setPower(hsb == OnOffType.ON, state);
|
||||
} else if (command instanceof DecimalType) {
|
||||
DecimalType d = (DecimalType) command;
|
||||
this.setBrightness(d.intValue(), state);
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
this.changeBrightness((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE ? 1 : -1,
|
||||
state);
|
||||
} else {
|
||||
logger.error("CHANNEL_BRIGHTNESS channel only supports OnOffType/IncreaseDecreaseType/DecimalType");
|
||||
}
|
||||
updateState(MilightBindingConstants.CHANNEL_COLOR, new HSBType(new DecimalType(state.hue360),
|
||||
new PercentType(state.saturation), new PercentType(state.brightness)));
|
||||
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_SATURATION: {
|
||||
if (command instanceof OnOffType) {
|
||||
OnOffType s = (OnOffType) command;
|
||||
this.setSaturation((s == OnOffType.ON) ? 100 : 0, state);
|
||||
} else if (command instanceof DecimalType) {
|
||||
DecimalType d = (DecimalType) command;
|
||||
this.setSaturation(d.intValue(), state);
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
this.changeSaturation((IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE ? 1 : -1,
|
||||
state);
|
||||
} else {
|
||||
logger.error("CHANNEL_SATURATION channel only supports OnOffType/IncreaseDecreaseType/DecimalType");
|
||||
}
|
||||
updateState(MilightBindingConstants.CHANNEL_COLOR, new HSBType(new DecimalType(state.hue360),
|
||||
new PercentType(state.saturation), new PercentType(state.brightness)));
|
||||
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_TEMP: {
|
||||
if (command instanceof OnOffType) {
|
||||
OnOffType s = (OnOffType) command;
|
||||
this.setColorTemperature((s == OnOffType.ON) ? 100 : 0, state);
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
this.changeColorTemperature(
|
||||
(IncreaseDecreaseType) command == IncreaseDecreaseType.INCREASE ? 1 : -1, state);
|
||||
} else if (command instanceof DecimalType) {
|
||||
DecimalType d = (DecimalType) command;
|
||||
this.setColorTemperature(d.intValue(), state);
|
||||
} else {
|
||||
logger.error("CHANNEL_TEMP channel only supports OnOffType/IncreaseDecreaseType/DecimalType");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_SPEED_REL: {
|
||||
if (command instanceof IncreaseDecreaseType) {
|
||||
IncreaseDecreaseType id = (IncreaseDecreaseType) command;
|
||||
if (id == IncreaseDecreaseType.INCREASE) {
|
||||
this.changeSpeed(1, state);
|
||||
} else if (id == IncreaseDecreaseType.DECREASE) {
|
||||
this.changeSpeed(-1, state);
|
||||
}
|
||||
} else {
|
||||
logger.error("CHANNEL_SPEED channel only supports IncreaseDecreaseType");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_ANIMATION_MODE: {
|
||||
if (command instanceof DecimalType) {
|
||||
DecimalType d = (DecimalType) command;
|
||||
this.setLedMode(d.intValue(), state);
|
||||
} else {
|
||||
logger.error("Animation mode channel only supports DecimalType");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_ANIMATION_MODE_REL: {
|
||||
if (command instanceof IncreaseDecreaseType) {
|
||||
IncreaseDecreaseType id = (IncreaseDecreaseType) command;
|
||||
if (id == IncreaseDecreaseType.INCREASE) {
|
||||
this.nextAnimationMode(state);
|
||||
} else if (id == IncreaseDecreaseType.DECREASE) {
|
||||
this.previousAnimationMode(state);
|
||||
}
|
||||
} else {
|
||||
logger.error("Relative animation mode channel only supports IncreaseDecreaseType");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.error("Channel unknown {}", channelUID.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bride handler.
|
||||
*/
|
||||
public @Nullable AbstractBridgeHandler getBridgeHandler() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
return null;
|
||||
}
|
||||
return (AbstractBridgeHandler) bridge.getHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bridge status.
|
||||
*/
|
||||
public ThingStatusInfo getBridgeStatus() {
|
||||
Bridge b = getBridge();
|
||||
if (b != null) {
|
||||
return b.getStatusInfo();
|
||||
} else {
|
||||
return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique command id for the {@see QueuedSend}. It incorporates the bridge, zone, bulb type and command
|
||||
* category.
|
||||
*
|
||||
* @param commandCategory The category of the command.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int uidc(int commandCategory) {
|
||||
return (bridgeOffset + config.zone + typeOffset + 1) * 64 + commandCategory;
|
||||
}
|
||||
|
||||
protected void start(AbstractBridgeHandler handler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(@NonNull ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
return;
|
||||
}
|
||||
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
AbstractBridgeHandler h = getBridgeHandler();
|
||||
if (h == null) {
|
||||
logger.warn("Bridge handler not found!");
|
||||
return;
|
||||
}
|
||||
final InetAddress inetAddress = h.address;
|
||||
if (inetAddress == null) {
|
||||
logger.warn("Bridge handler has not yet determined the IP address!");
|
||||
return;
|
||||
}
|
||||
|
||||
state.reset();
|
||||
configUpdated(h, inetAddress);
|
||||
|
||||
if (h.getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
start(h);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the bridge if a configuration update happened after initialisation has been done
|
||||
*
|
||||
* @param h The bridge handler
|
||||
*/
|
||||
public void configUpdated(AbstractBridgeHandler h, InetAddress address) {
|
||||
this.port = h.config.port;
|
||||
this.address = address;
|
||||
this.socket = h.socket;
|
||||
this.delayTimeMS = h.config.delayTime;
|
||||
this.repeatTimes = h.config.repeat;
|
||||
this.bridgeOffset = h.bridgeOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
config = getConfigAs(LedHandlerConfig.class);
|
||||
bridgeStatusChanged(getBridgeStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.QueueItem;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class implements common functionality for Milight/Easybulb bulbs of protocol version 3.
|
||||
* Most of the implementation is found in the specific bulb classes though.
|
||||
* The class is state-less, use {@link MilightThingState} instead.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractLedV3Handler extends AbstractLedHandler {
|
||||
public static final int MAX_ANIM_MODES = 10;
|
||||
protected final Logger logger = LoggerFactory.getLogger(AbstractLedV3Handler.class);
|
||||
|
||||
public AbstractLedV3Handler(Thing thing, QueuedSend sendQueue, int typeOffset) {
|
||||
super(thing, sendQueue, typeOffset);
|
||||
}
|
||||
|
||||
// we have to map [0,360] to [0,0xFF], where red equals hue=0 and the milight color 0xB0 (=176)
|
||||
public static byte makeColor(int hue) {
|
||||
int mHue = (360 + 248 - hue) % 360; // invert and shift
|
||||
return (byte) (mHue * 255 / 360); // map to 256 values
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLedMode(int mode, MilightThingState state) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSaturation(int value, MilightThingState state) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSaturation(int relativeSaturation, MilightThingState state) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
protected QueueItem createRepeatable(byte[] data) {
|
||||
return QueueItem.createRepeatable(socket, delayTimeMS, repeatTimes, address, port, data);
|
||||
}
|
||||
|
||||
protected QueueItem createRepeatable(int uidc, byte[] data) {
|
||||
return new QueueItem(socket, uidc, data, true, delayTimeMS, repeatTimes, address, port);
|
||||
}
|
||||
|
||||
protected QueueItem createNonRepeatable(byte[] data) {
|
||||
return QueueItem.createNonRepeatable(socket, delayTimeMS, address, port, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueueItem;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Implements basic V6 bulb functionally. But commands are different for different v6 bulbs, so almost all the work is
|
||||
* done in subclasses.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractLedV6Handler extends AbstractLedHandler {
|
||||
protected final Logger logger = LoggerFactory.getLogger(AbstractLedV6Handler.class);
|
||||
|
||||
protected static final int MAX_BR = 100; // Maximum brightness (0x64)
|
||||
protected static final int MAX_SAT = 100; // Maximum saturation (0x64)
|
||||
protected static final int MAX_TEMP = 100; // Maximum colour temperature (0x64)
|
||||
|
||||
protected @NonNullByDefault({}) MilightV6SessionManager session;
|
||||
|
||||
public AbstractLedV6Handler(Thing thing, QueuedSend sendQueue, int typeOffset) {
|
||||
super(thing, sendQueue, typeOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void start(AbstractBridgeHandler handler) {
|
||||
BridgeV6Handler h = (BridgeV6Handler) handler;
|
||||
session = h.getSessionManager();
|
||||
}
|
||||
|
||||
protected abstract byte getAddr();
|
||||
|
||||
protected abstract byte getBrCmd();
|
||||
|
||||
@Override
|
||||
public void setHSB(int hue, int saturation, int brightness, MilightThingState state) {
|
||||
if (hue > 360 || hue < 0) {
|
||||
logger.error("Hue argument out of range");
|
||||
return;
|
||||
}
|
||||
|
||||
// 0xFF = Red, D9 = Lavender, BA = Blue, 85 = Aqua, 7A = Green, 54 = Lime, 3B = Yellow, 1E = Orange
|
||||
// we have to map [0,360] to [0,0xFF], where red equals hue=0 and the milight color 0xFF (=255)
|
||||
// Integer milightColorNo = (256 + 0xFF - (int) (hue / 360.0 * 255.0)) % 256;
|
||||
|
||||
// Compute destination hue and current hue value, each mapped to 256 values.
|
||||
// int cHue = state.hue360 * 255 / 360; // map to 256 values
|
||||
int dHue = hue * 255 / 360; // map to 256 values
|
||||
sendRepeatableCat(ProtocolConstants.CAT_COLOR_SET, 1, dHue, dHue, dHue, dHue);
|
||||
|
||||
state.hue360 = hue;
|
||||
|
||||
if (brightness != -1) {
|
||||
setBrightness(brightness, state);
|
||||
}
|
||||
|
||||
if (saturation != -1) {
|
||||
setSaturation(saturation, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int newvalue, MilightThingState state) {
|
||||
int value = Math.min(Math.max(newvalue, 0), 100);
|
||||
|
||||
if (value == 0) {
|
||||
setPower(false, state);
|
||||
} else if (state.brightness == 0) {
|
||||
// If if was dimmed to minimum (off), turn it on again
|
||||
setPower(true, state);
|
||||
}
|
||||
|
||||
int br = (value * MAX_BR) / 100;
|
||||
br = Math.min(br, MAX_BR);
|
||||
br = Math.max(br, 0);
|
||||
sendRepeatableCat(ProtocolConstants.CAT_BRIGHTNESS_SET, getBrCmd(), br);
|
||||
|
||||
state.brightness = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeColorTemperature(int colorTempRelative, MilightThingState state) {
|
||||
if (!session.isValid()) {
|
||||
logger.error("Bridge communication session not valid yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (colorTempRelative == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int ct = (state.colorTemperature * MAX_TEMP) / 100 + colorTempRelative;
|
||||
ct = Math.min(ct, MAX_TEMP);
|
||||
ct = Math.max(ct, 0);
|
||||
setColorTemperature(ct * 100 / MAX_TEMP, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeBrightness(int relativeBrightness, MilightThingState state) {
|
||||
if (!session.isValid()) {
|
||||
logger.error("Bridge communication session not valid yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relativeBrightness == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int br = (state.brightness * MAX_BR) / 100 + relativeBrightness;
|
||||
br = Math.min(br, MAX_BR);
|
||||
br = Math.max(br, 0);
|
||||
|
||||
setBrightness(br * 100 / MAX_BR, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSaturation(int relativeSaturation, MilightThingState state) {
|
||||
if (!session.isValid()) {
|
||||
logger.error("Bridge communication session not valid yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (relativeSaturation == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int br = (state.brightness * MAX_BR) / 100 + relativeSaturation;
|
||||
br = Math.min(br, MAX_BR);
|
||||
br = Math.max(br, 0);
|
||||
|
||||
setSaturation(br * 100 / MAX_BR, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void previousAnimationMode(MilightThingState state) {
|
||||
if (!session.isValid()) {
|
||||
logger.error("Bridge communication session not valid yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
int mode = state.animationMode - 1;
|
||||
mode = Math.min(mode, 9);
|
||||
mode = Math.max(mode, 1);
|
||||
|
||||
setLedMode(mode, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextAnimationMode(MilightThingState state) {
|
||||
if (!session.isValid()) {
|
||||
logger.error("Bridge communication session not valid yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
int mode = state.animationMode + 1;
|
||||
mode = Math.min(mode, 9);
|
||||
mode = Math.max(mode, 1);
|
||||
|
||||
setLedMode(mode, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
super.handleCommand(channelUID, command);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case MilightBindingConstants.CHANNEL_LINKLED: {
|
||||
sendQueue.queue(new QueueItem(socket, uidc(ProtocolConstants.CAT_LINK),
|
||||
session.makeLink(getAddr(), config.zone, true), true, delayTimeMS, repeatTimes, address, port));
|
||||
break;
|
||||
}
|
||||
case MilightBindingConstants.CHANNEL_UNLINKLED: {
|
||||
sendQueue.queue(new QueueItem(socket, uidc(ProtocolConstants.CAT_LINK),
|
||||
session.makeLink(getAddr(), config.zone, false), true, delayTimeMS, repeatTimes, address,
|
||||
port));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
super.handleCommand(channelUID, command);
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendNonRepeatable(int... data) {
|
||||
sendQueue.queue(QueueItem.createNonRepeatable(socket, delayTimeMS, address, port,
|
||||
session.makeCommand(getAddr(), config.zone, data)));
|
||||
}
|
||||
|
||||
protected void sendRepeatableCat(int cat, int... data) {
|
||||
sendQueue.queue(new QueueItem(socket, uidc(cat), session.makeCommand(getAddr(), config.zone, data), true,
|
||||
delayTimeMS, repeatTimes, address, port));
|
||||
}
|
||||
|
||||
protected void sendRepeatable(int... data) {
|
||||
sendQueue.queue(QueueItem.createRepeatable(socket, delayTimeMS, repeatTimes, address, port,
|
||||
session.makeCommand(getAddr(), config.zone, data)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
/**
|
||||
* The configuration for all bridge types.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class BridgeHandlerConfig {
|
||||
public static final String CONFIG_BRIDGE_ID = "bridgeid";
|
||||
String bridgeid = "";
|
||||
public static final String CONFIG_HOST_NAME = "host";
|
||||
String host = "";
|
||||
int refreshTime = 5000;
|
||||
int port = 0;
|
||||
int passwordByte1 = 0;
|
||||
int passwordByte2 = 0;
|
||||
int repeat = 3;
|
||||
int delayTime = 100;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
|
||||
/**
|
||||
* The {@link BridgeV3Handler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BridgeV3Handler extends AbstractBridgeHandler {
|
||||
protected final DatagramPacket discoverPacketV3;
|
||||
protected final byte[] buffer = new byte[1024];
|
||||
protected final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
private @Nullable ScheduledFuture<?> running;
|
||||
|
||||
public BridgeV3Handler(Bridge bridge, int bridgeOffset) {
|
||||
super(bridge, bridgeOffset);
|
||||
discoverPacketV3 = new DatagramPacket(MilightBindingConstants.DISCOVER_MSG_V3,
|
||||
MilightBindingConstants.DISCOVER_MSG_V3.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thingUpdated(Thing thing) {
|
||||
super.thingUpdated(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a discovery object and the send queue. The initial IP address may be null
|
||||
* or is not matching with the real IP address of the bridge. The discovery class will send
|
||||
* a broadcast packet to find the bridge with the respective bridge ID. The response in bridgeDetected()
|
||||
* may lead to a recreation of the send queue object.
|
||||
*
|
||||
* The keep alive timer that is also setup here, will send keep alive packets periodically.
|
||||
* If the bridge doesn't respond anymore (e.g. DHCP IP change), the initial session handshake
|
||||
* starts all over again.
|
||||
*/
|
||||
@Override
|
||||
protected void startConnectAndKeepAlive() {
|
||||
if (address == null) {
|
||||
if (!config.bridgeid.matches("^([0-9A-Fa-f]{12})$")) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridgeID invalid!");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
address = InetAddress.getByAddress(new byte[] { (byte) 255, (byte) 255, (byte) 255, (byte) 255 });
|
||||
} catch (UnknownHostException neverHappens) {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.port == 0) {
|
||||
config.port = MilightBindingConstants.PORT_VER3;
|
||||
}
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(null);
|
||||
socket.setReuseAddress(true);
|
||||
socket.setBroadcast(true);
|
||||
socket.bind(null);
|
||||
} catch (SocketException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
running = scheduler.scheduleWithFixedDelay(this::receive, 0, config.refreshTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
protected void stopKeepAlive() {
|
||||
if (running != null) {
|
||||
running.cancel(false);
|
||||
running = null;
|
||||
}
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopKeepAlive();
|
||||
}
|
||||
|
||||
private void receive() {
|
||||
try {
|
||||
discoverPacketV3.setAddress(address);
|
||||
discoverPacketV3.setPort(MilightBindingConstants.PORT_DISCOVER);
|
||||
|
||||
final int attempts = 5;
|
||||
int timeoutsCounter = 0;
|
||||
for (timeoutsCounter = 1; timeoutsCounter <= attempts; ++timeoutsCounter) {
|
||||
try {
|
||||
packet.setLength(buffer.length);
|
||||
socket.setSoTimeout(500 * timeoutsCounter);
|
||||
socket.send(discoverPacketV3);
|
||||
socket.receive(packet);
|
||||
} catch (SocketTimeoutException e) {
|
||||
continue;
|
||||
}
|
||||
// We expect packets with a format like this: 10.1.1.27,ACCF23F57AD4,HF-LPB100
|
||||
final String received = new String(packet.getData());
|
||||
final String[] msg = received.split(",");
|
||||
|
||||
if (msg.length != 2 && msg.length != 3) {
|
||||
// That data packet does not belong to a Milight bridge. Just ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// First argument is the IP
|
||||
try {
|
||||
InetAddress.getByName(msg[0]);
|
||||
} catch (UnknownHostException ignored) {
|
||||
// That data packet does not belong to a Milight bridge, we expect an IP address as first
|
||||
// argument. Just ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Second argument is the MAC address
|
||||
if (msg[1].length() != 12) {
|
||||
// That data packet does not belong to a Milight bridge, we expect a MAC address as second
|
||||
// argument.
|
||||
// Just ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
final InetAddress addressOfBridge = ((InetSocketAddress) packet.getSocketAddress()).getAddress();
|
||||
final String bridgeID = msg[1];
|
||||
|
||||
if (!config.bridgeid.isEmpty() && !bridgeID.equals(config.bridgeid)) {
|
||||
// We found a bridge, but it is not the one that is handled by this handler
|
||||
if (!config.host.isEmpty()) { // The user has set a host address -> but wrong bridge found!
|
||||
stopKeepAlive();
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Wrong bridge found on host address. Change bridgeid or host configuration.");
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// IP address has changed, reestablish communication
|
||||
if (!addressOfBridge.equals(this.address)) {
|
||||
this.address = addressOfBridge;
|
||||
Configuration c = editConfiguration();
|
||||
c.put(BridgeHandlerConfig.CONFIG_HOST_NAME, addressOfBridge.getHostAddress());
|
||||
preventReinit = true;
|
||||
updateConfiguration(c);
|
||||
preventReinit = false;
|
||||
} else if (config.bridgeid.isEmpty()) { // bridge id was not set and is now known. Store it.
|
||||
config.bridgeid = bridgeID;
|
||||
Configuration c = editConfiguration();
|
||||
c.put(BridgeHandlerConfig.CONFIG_BRIDGE_ID, bridgeID);
|
||||
preventReinit = true;
|
||||
updateConfiguration(c);
|
||||
preventReinit = false;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
break;
|
||||
}
|
||||
if (timeoutsCounter > attempts) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Bridge did not respond!");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager.ISessionState;
|
||||
import org.openhab.binding.milight.internal.protocol.MilightV6SessionManager.SessionState;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
|
||||
/**
|
||||
* The {@link BridgeV6Handler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BridgeV6Handler extends AbstractBridgeHandler implements ISessionState {
|
||||
private @NonNullByDefault({}) MilightV6SessionManager session;
|
||||
final DateTimeFormatter timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
|
||||
private String offlineReason = "";
|
||||
private @Nullable ScheduledFuture<?> scheduledFuture;
|
||||
|
||||
public BridgeV6Handler(Bridge bridge, int bridgeOffset) {
|
||||
super(bridge, bridgeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a session manager object and the send queue. The initial IP address may be null
|
||||
* or is not matching with the real IP address of the bridge. The session manager will send
|
||||
* a broadcast packet to find the bridge with the respective bridge ID and will change the
|
||||
* IP address of the send queue object accordingly.
|
||||
*
|
||||
* The keep alive timer that is also setup here, will send keep alive packets periodically.
|
||||
* If the bridge doesn't respond anymore (e.g. DHCP IP change), the initial session handshake
|
||||
* starts all over again.
|
||||
*/
|
||||
@Override
|
||||
protected void startConnectAndKeepAlive() {
|
||||
if (!config.bridgeid.matches("^([0-9A-Fa-f]{12})$")) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridgeID invalid!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.port == 0) {
|
||||
config.port = MilightBindingConstants.PORT_VER6;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for session");
|
||||
int refreshTime = Math.max(Math.min(config.refreshTime, MilightV6SessionManager.TIMEOUT_MS), 100);
|
||||
this.session = new MilightV6SessionManager(config.bridgeid, this, address, config.port, refreshTime,
|
||||
new byte[] { (byte) config.passwordByte1, (byte) config.passwordByte2 });
|
||||
session.start().thenAccept(d -> this.socket = d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
stopMakeOfflineTimer();
|
||||
try {
|
||||
session.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
this.session = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public MilightV6SessionManager getSessionManager() {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionStateChanged(SessionState state, @Nullable InetAddress newAddress) {
|
||||
stopMakeOfflineTimer();
|
||||
switch (state) {
|
||||
case SESSION_VALID_KEEP_ALIVE:
|
||||
preventReinit = true;
|
||||
Instant lastSessionTime = session.getLastSessionValidConfirmation();
|
||||
LocalDateTime date = LocalDateTime.ofInstant(lastSessionTime, ZoneId.systemDefault());
|
||||
updateProperty(MilightBindingConstants.PROPERTY_SESSIONCONFIRMED, date.format(timeFormat));
|
||||
preventReinit = false;
|
||||
break;
|
||||
case SESSION_VALID:
|
||||
if (newAddress == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address received");
|
||||
break;
|
||||
}
|
||||
if (!newAddress.equals(address) || !thing.getStatus().equals(ThingStatus.ONLINE)) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
this.address = newAddress;
|
||||
// As soon as the session is valid, update the user visible configuration of the host IP.
|
||||
Configuration c = editConfiguration();
|
||||
c.put(BridgeHandlerConfig.CONFIG_HOST_NAME, newAddress.getHostAddress());
|
||||
thing.setProperty(MilightBindingConstants.PROPERTY_SESSIONID, session.getSession());
|
||||
thing.setProperty(MilightBindingConstants.PROPERTY_SESSIONCONFIRMED,
|
||||
String.valueOf(session.getLastSessionValidConfirmation()));
|
||||
preventReinit = true;
|
||||
updateConfiguration(c);
|
||||
preventReinit = false;
|
||||
}
|
||||
break;
|
||||
case SESSION_INVALID:
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Session could not be established");
|
||||
|
||||
break;
|
||||
default:
|
||||
// Delay putting the session offline
|
||||
offlineReason = state.name();
|
||||
scheduledFuture = scheduler.schedule(
|
||||
() -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, offlineReason),
|
||||
1000, TimeUnit.MILLISECONDS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopMakeOfflineTimer() {
|
||||
ScheduledFuture<?> future = scheduledFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
scheduledFuture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
/**
|
||||
* The configuration for all led types.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class LedHandlerConfig {
|
||||
public static final String CONFIG_ZONE = "zone";
|
||||
int zone = 0;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
|
||||
/**
|
||||
* The {@link LedHandlerInterface} defines the general interface for all Milight bulbs.
|
||||
* The different versions might support additional functionality.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface LedHandlerInterface {
|
||||
/**
|
||||
* Set the color value of this bulb.
|
||||
*
|
||||
* @param hue A value from 0 to 360
|
||||
* @param saturation A saturation value. Can be -1 if not known
|
||||
* @param brightness A brightness value. Can be -1 if not known
|
||||
* @param state The changed values will be written back to the state
|
||||
*/
|
||||
void setHSB(int hue, int saturation, int brightness, MilightThingState state);
|
||||
|
||||
/**
|
||||
* Enable/Disable the bulb.
|
||||
*
|
||||
* @param on On/Off
|
||||
* @param state The changed values will be written back to the state
|
||||
*/
|
||||
void setPower(boolean on, MilightThingState state);
|
||||
|
||||
/**
|
||||
* Switches to white mode (disables color leds).
|
||||
*
|
||||
* @param state The changed values will be written back to the state
|
||||
*/
|
||||
void whiteMode(MilightThingState state);
|
||||
|
||||
/**
|
||||
* Switches to night mode (low current for all leds).
|
||||
*
|
||||
* @param state The changed values will be written back to the state
|
||||
*/
|
||||
void nightMode(MilightThingState state);
|
||||
|
||||
/**
|
||||
* Sets the color temperature of the bulb.
|
||||
*
|
||||
* @param colorTemp Color temperature percentage
|
||||
* @param state The changed values will be written back to the state
|
||||
*/
|
||||
void setColorTemperature(int colorTemp, MilightThingState state);
|
||||
|
||||
void changeColorTemperature(int colorTempRelative, MilightThingState state);
|
||||
|
||||
/**
|
||||
* Sets the brightness of the bulb.
|
||||
*
|
||||
* @param value brightness percentage
|
||||
* @param state The changed values will be written back to the state
|
||||
*/
|
||||
void setBrightness(int value, MilightThingState state);
|
||||
|
||||
void changeBrightness(int relativeBrightness, MilightThingState state);
|
||||
|
||||
void setSaturation(int value, MilightThingState state);
|
||||
|
||||
void changeSaturation(int relativeSaturation, MilightThingState state);
|
||||
|
||||
void setLedMode(int mode, MilightThingState state);
|
||||
|
||||
void previousAnimationMode(MilightThingState state);
|
||||
|
||||
void nextAnimationMode(MilightThingState state);
|
||||
|
||||
void changeSpeed(int relativeSpeed, MilightThingState state);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueueItem;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Implements functionality for the very first RGB bulbs for protocol version 2.
|
||||
* A lot of stuff is not supported by this type of bulbs and they are not auto discovered
|
||||
* and can only be add manually in the things file.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightV2RGBHandler extends AbstractLedHandler {
|
||||
protected final Logger logger = LoggerFactory.getLogger(MilightV2RGBHandler.class);
|
||||
protected static final int BR_LEVELS = 9;
|
||||
|
||||
public MilightV2RGBHandler(Thing thing, QueuedSend sendQueue) {
|
||||
super(thing, sendQueue, 5);
|
||||
}
|
||||
|
||||
protected QueueItem createRepeatable(byte[] data) {
|
||||
return QueueItem.createRepeatable(socket, delayTimeMS, repeatTimes, address, port, data);
|
||||
}
|
||||
|
||||
protected QueueItem createRepeatable(int uidc, byte[] data) {
|
||||
return new QueueItem(socket, uidc, data, true, delayTimeMS, repeatTimes, address, port);
|
||||
}
|
||||
|
||||
protected QueueItem createNonRepeatable(byte[] data) {
|
||||
return QueueItem.createNonRepeatable(socket, delayTimeMS, address, port, data);
|
||||
}
|
||||
|
||||
// We have no real saturation control for RGB bulbs. If the saturation is under a 50% threshold
|
||||
// we just change to white mode instead.
|
||||
@Override
|
||||
public void setHSB(int hue, int saturation, int brightness, MilightThingState state) {
|
||||
if (saturation < 50) {
|
||||
whiteMode(state);
|
||||
} else {
|
||||
setPower(true, state);
|
||||
state.hue360 = hue;
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_COLOR_SET),
|
||||
new byte[] { 0x20, AbstractLedV3Handler.makeColor(hue), 0x55 }));
|
||||
|
||||
if (brightness != -1) {
|
||||
setBrightness(brightness, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(boolean on, MilightThingState state) {
|
||||
if (on) {
|
||||
logger.debug("milight: sendOn");
|
||||
byte messageBytes[] = null;
|
||||
// message rgb bulbs ON
|
||||
messageBytes = new byte[] { 0x22, 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE), messageBytes));
|
||||
} else {
|
||||
logger.debug("milight: sendOff");
|
||||
byte messageBytes[] = null;
|
||||
|
||||
// message rgb bulbs OFF
|
||||
messageBytes = new byte[] { 0x21, 0x00, 0x55 };
|
||||
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE), messageBytes));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whiteMode(MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nightMode(MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeColorTemperature(int colorTempRelative, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLedMode(int mode, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSaturation(int value, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSaturation(int relativeSaturation, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorTemperature(int colorTemp, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int value, MilightThingState state) {
|
||||
if (value <= 0) {
|
||||
setPower(false, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value > state.brightness) {
|
||||
int repeatCount = (value - state.brightness) / 10;
|
||||
for (int i = 0; i < repeatCount; i++) {
|
||||
|
||||
changeBrightness(+1, state);
|
||||
|
||||
}
|
||||
} else if (value < state.brightness) {
|
||||
int repeatCount = (state.brightness - value) / 10;
|
||||
for (int i = 0; i < repeatCount; i++) {
|
||||
|
||||
changeBrightness(-1, state);
|
||||
}
|
||||
}
|
||||
|
||||
state.brightness = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeBrightness(int relativeBrightness, MilightThingState state) {
|
||||
int newPercent = state.brightness + relativeBrightness;
|
||||
if (newPercent < 0) {
|
||||
newPercent = 0;
|
||||
}
|
||||
if (newPercent > 100) {
|
||||
newPercent = 100;
|
||||
}
|
||||
if (state.brightness != -1 && newPercent == 0) {
|
||||
setPower(false, state);
|
||||
} else {
|
||||
setPower(true, state);
|
||||
int steps = (int) Math.abs(Math.floor(relativeBrightness * BR_LEVELS / 100.0));
|
||||
for (int s = 0; s < steps; ++s) {
|
||||
byte[] t = { (byte) (relativeBrightness < 0 ? 0x24 : 0x23), 0x00, 0x55 };
|
||||
sendQueue.queue(createNonRepeatable(t));
|
||||
}
|
||||
}
|
||||
state.brightness = newPercent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSpeed(int relativeSpeed, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void previousAnimationMode(MilightThingState state) {
|
||||
setPower(true, state);
|
||||
sendQueue.queue(createNonRepeatable(new byte[] { 0x28, 0x00, 0x55 }));
|
||||
state.animationMode = Math.min(state.animationMode - 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextAnimationMode(MilightThingState state) {
|
||||
setPower(true, state);
|
||||
sendQueue.queue(createNonRepeatable(new byte[] { 0x27, 0x00, 0x55 }));
|
||||
state.animationMode = Math.max(state.animationMode + 1, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* Implements functionality for the RGB/White bulbs for protocol version 3.
|
||||
* Color temperature and saturation are not supported for this type of bulbs.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class MilightV3RGBWHandler extends AbstractLedV3Handler {
|
||||
protected static final int BRIGHTNESS_LEVELS = 26;
|
||||
|
||||
private static final byte COMMAND_ON[] = { (byte) 0x42, (byte) 0x45, (byte) 0x47, (byte) 0x49, (byte) 0x4B };
|
||||
private static final byte COMMAND_OFF[] = { (byte) 0x41, (byte) 0x46, (byte) 0x48, (byte) 0x4A, (byte) 0x4C };
|
||||
private static final byte COMMAND_WHITEMODE[] = { (byte) 0xC2, (byte) 0xC5, (byte) 0xC7, (byte) 0xC9, (byte) 0xCB };
|
||||
private static final byte NIGHTMODE_FIRST[] = { 0x41, 0x46, 0x48, 0x4A, 0x4C };
|
||||
private static final byte NIGHTMODE_SECOND[] = { (byte) 0xC1, (byte) 0xC6, (byte) 0xC8, (byte) 0xCA, (byte) 0xCC };
|
||||
private static final byte NEXT_ANIMATION_MODE[] = { 0x4D, 0x00, 0x55 };
|
||||
|
||||
public MilightV3RGBWHandler(Thing thing, QueuedSend sendQueue) {
|
||||
super(thing, sendQueue, 10);
|
||||
}
|
||||
|
||||
// We have no real saturation control for RGBW bulbs. If the saturation is under a 50% threshold
|
||||
// we just change to white mode instead.
|
||||
@Override
|
||||
public void setHSB(int hue, int saturation, int brightness, MilightThingState state) {
|
||||
if (saturation < 50) {
|
||||
whiteMode(state);
|
||||
} else {
|
||||
state.hue360 = hue;
|
||||
final byte messageBytes[] = new byte[] { 0x40, makeColor(hue), 0x55 };
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_COLOR_SET), cOn).addRepeatable(messageBytes));
|
||||
}
|
||||
|
||||
if (brightness != -1) {
|
||||
setBrightness(brightness, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(boolean on, MilightThingState state) {
|
||||
if (on) {
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE),
|
||||
new byte[] { COMMAND_ON[config.zone], 0x00, 0x55 }));
|
||||
} else {
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE),
|
||||
new byte[] { COMMAND_OFF[config.zone], 0x00, 0x55 }));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whiteMode(MilightThingState state) {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
final byte cWhite[] = { COMMAND_WHITEMODE[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_WHITEMODE), cOn).addRepeatable(cWhite));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nightMode(MilightThingState state) {
|
||||
final byte cN1[] = { NIGHTMODE_FIRST[config.zone], 0x00, 0x55 };
|
||||
final byte cN2[] = { NIGHTMODE_SECOND[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE), cN1).addRepeatable(cN2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorTemperature(int colorTemp, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeColorTemperature(int colorTempRelative, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBrightness(int newvalue, MilightThingState state) {
|
||||
int value = Math.min(Math.max(newvalue, 0), 100);
|
||||
|
||||
if (value == 0) {
|
||||
state.brightness = value;
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE),
|
||||
new byte[] { COMMAND_OFF[config.zone], 0x00, 0x55 }));
|
||||
return;
|
||||
}
|
||||
|
||||
int br = (int) Math.ceil((value * BRIGHTNESS_LEVELS) / 100.0) + 1;
|
||||
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_BRIGHTNESS_SET), cOn)
|
||||
.addRepeatable(new byte[] { 0x4E, (byte) br, 0x55 }));
|
||||
|
||||
state.brightness = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeBrightness(int relativeBrightness, MilightThingState state) {
|
||||
setBrightness(Math.max(0, Math.min(100, state.brightness + relativeBrightness)), state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSpeed(int relativeSpeed, MilightThingState state) {
|
||||
if (relativeSpeed == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
final byte cSpeed[] = { (byte) (relativeSpeed > 0 ? 0x44 : 0x43), 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(cSpeed));
|
||||
}
|
||||
|
||||
// This bulb actually doesn't implement a previous animation mode command. We just use the next mode command
|
||||
// instead.
|
||||
@Override
|
||||
public void previousAnimationMode(MilightThingState state) {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(NEXT_ANIMATION_MODE));
|
||||
state.animationMode = (state.animationMode + 1) % (MAX_ANIM_MODES + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextAnimationMode(MilightThingState state) {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(NEXT_ANIMATION_MODE));
|
||||
state.animationMode = (state.animationMode + 1) % (MAX_ANIM_MODES + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueueItem;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* Implements functionality for the Dual White bulbs for protocol version 3.
|
||||
* Color temperature is supported for this type of bulbs.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightV3WhiteHandler extends AbstractLedV3Handler {
|
||||
protected static final int BRIGHTNESS_LEVELS = 11;
|
||||
|
||||
public MilightV3WhiteHandler(Thing thing, QueuedSend sendQueue) {
|
||||
super(thing, sendQueue, 0);
|
||||
}
|
||||
|
||||
private static final byte COMMAND_FULL[] = { (byte) 0xB5, (byte) 0xB8, (byte) 0xBD, (byte) 0xB7, (byte) 0xB2 };
|
||||
private static final byte COMMAND_ON[] = { (byte) 0x35, (byte) 0x38, (byte) 0x3D, (byte) 0x37, (byte) 0x32 };
|
||||
private static final byte COMMAND_OFF[] = { (byte) 0x39, (byte) 0x3B, (byte) 0x33, (byte) 0x3A, (byte) 0x36 };
|
||||
private static final byte COMMAND_NIGHTMODE[] = { (byte) 0xB9, (byte) 0xBB, (byte) 0xB3, (byte) 0xBA, (byte) 0xB6 };
|
||||
private static final byte PREV_ANIMATION_MODE[] = { 0x27, 0x00, 0x55 };
|
||||
private static final byte NEXT_ANIMATION_MODE[] = { 0x27, 0x00, 0x55 };
|
||||
|
||||
protected void setFull(int zone, MilightThingState state) {
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_BRIGHTNESS_SET),
|
||||
new byte[] { COMMAND_FULL[zone], 0x00, 0x55 }));
|
||||
state.brightness = 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHSB(int hue, int saturation, int brightness, MilightThingState state) {
|
||||
// This bulb type only supports a brightness value
|
||||
if (brightness != -1) {
|
||||
setBrightness(brightness, state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(boolean on, MilightThingState state) {
|
||||
if (on) {
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE),
|
||||
new byte[] { COMMAND_ON[config.zone], 0x00, 0x55 }));
|
||||
} else {
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE),
|
||||
new byte[] { COMMAND_OFF[config.zone], 0x00, 0x55 }));
|
||||
state.brightness = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whiteMode(MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nightMode(MilightThingState state) {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
final byte cNight[] = { COMMAND_NIGHTMODE[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE), cOn).addRepeatable(cNight));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorTemperature(int colorTemp, MilightThingState state) {
|
||||
// White Bulbs: 11 levels of temperature + Off.
|
||||
int newLevel;
|
||||
int oldLevel;
|
||||
// Reset bulb to known state
|
||||
if (colorTemp <= 0) {
|
||||
colorTemp = 0;
|
||||
newLevel = 1;
|
||||
oldLevel = BRIGHTNESS_LEVELS;
|
||||
} else if (colorTemp >= 100) {
|
||||
colorTemp = 100;
|
||||
newLevel = BRIGHTNESS_LEVELS;
|
||||
oldLevel = 1;
|
||||
} else {
|
||||
newLevel = (int) Math.ceil((colorTemp * BRIGHTNESS_LEVELS) / 100.0);
|
||||
oldLevel = (int) Math.ceil((state.colorTemperature * BRIGHTNESS_LEVELS) / 100.0);
|
||||
}
|
||||
|
||||
final int repeatCount = Math.abs(newLevel - oldLevel);
|
||||
if (newLevel > oldLevel) {
|
||||
for (int i = 0; i < repeatCount; i++) {
|
||||
changeColorTemperature(1, state);
|
||||
}
|
||||
} else if (newLevel < oldLevel) {
|
||||
for (int i = 0; i < repeatCount; i++) {
|
||||
changeColorTemperature(-1, state);
|
||||
}
|
||||
}
|
||||
|
||||
state.colorTemperature = colorTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeColorTemperature(int colorTempRelative, MilightThingState state) {
|
||||
state.colorTemperature = Math.min(100, Math.max(state.colorTemperature + colorTempRelative, 0));
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
final byte cTemp[] = { (byte) (colorTempRelative > 0 ? 0x3E : 0x3F), 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(cTemp));
|
||||
}
|
||||
|
||||
// This just emulates an absolute brightness command with the relative commands.
|
||||
@Override
|
||||
public void setBrightness(int value, MilightThingState state) {
|
||||
if (value <= 0) {
|
||||
setPower(false, state);
|
||||
return;
|
||||
} else if (value >= 100) {
|
||||
setFull(config.zone, state);
|
||||
return;
|
||||
}
|
||||
|
||||
// White Bulbs: 11 levels of brightness + Off.
|
||||
final int newLevel = (int) Math.ceil((value * BRIGHTNESS_LEVELS) / 100.0);
|
||||
|
||||
// When turning on start from full brightness
|
||||
int oldLevel;
|
||||
final byte cFull[] = { COMMAND_FULL[config.zone], 0x00, 0x55 };
|
||||
QueueItem item = createRepeatable(cFull);
|
||||
boolean skipFirst = false;
|
||||
|
||||
if (state.brightness == 0) {
|
||||
oldLevel = BRIGHTNESS_LEVELS;
|
||||
} else {
|
||||
oldLevel = (int) Math.ceil((state.brightness * BRIGHTNESS_LEVELS) / 100.0);
|
||||
skipFirst = true;
|
||||
|
||||
if (newLevel == oldLevel) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final int repeatCount = Math.abs(newLevel - oldLevel);
|
||||
|
||||
logger.debug("milight: dim from '{}' with command '{}' via '{}' steps.", String.valueOf(state.brightness),
|
||||
String.valueOf(value), repeatCount);
|
||||
|
||||
int op = newLevel > oldLevel ? +1 : -1;
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
for (int i = 0; i < repeatCount; i++) {
|
||||
final byte[] cBr = { (byte) (op < 0 ? 0x34 : 0x3C), 0x00, 0x55 };
|
||||
item = item.addRepeatable(cOn).addNonRepeatable(cBr);
|
||||
}
|
||||
|
||||
final QueueItem nextItem = item.next;
|
||||
if (nextItem != null && skipFirst) {
|
||||
sendQueue.queue(nextItem);
|
||||
} else {
|
||||
sendQueue.queue(item);
|
||||
}
|
||||
|
||||
state.brightness = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeBrightness(int relativeBrightness, MilightThingState state) {
|
||||
if (relativeBrightness == 0) {
|
||||
return;
|
||||
}
|
||||
state.brightness = Math.min(Math.max(state.brightness + relativeBrightness, 0), 100);
|
||||
|
||||
if (state.brightness == 0) {
|
||||
sendQueue.queue(createRepeatable(uidc(ProtocolConstants.CAT_POWER_MODE),
|
||||
new byte[] { COMMAND_OFF[config.zone], 0x00, 0x55 }));
|
||||
} else {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
final byte cBr[] = { (byte) (relativeBrightness < 0 ? 0x34 : 0x3C), 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(cBr));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSpeed(int relativeSpeed, MilightThingState state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void previousAnimationMode(MilightThingState state) {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(PREV_ANIMATION_MODE));
|
||||
state.animationMode = Math.max(state.animationMode - 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextAnimationMode(MilightThingState state) {
|
||||
final byte cOn[] = { COMMAND_ON[config.zone], 0x00, 0x55 };
|
||||
sendQueue.queue(createRepeatable(cOn).addNonRepeatable(NEXT_ANIMATION_MODE));
|
||||
state.animationMode = Math.min(state.animationMode + 1, MAX_ANIM_MODES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* Implements the RGB cold white / warm white bulb. It is the most feature rich bulb.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightV6RGBCWWWHandler extends AbstractLedV6Handler {
|
||||
private static final int ADDR = 0x08;
|
||||
|
||||
public MilightV6RGBCWWWHandler(Thing thing, QueuedSend sendQueue) {
|
||||
super(thing, sendQueue, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getAddr() {
|
||||
return ADDR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(boolean on, MilightThingState state) {
|
||||
sendRepeatableCat(ProtocolConstants.CAT_POWER_MODE, 4, on ? 1 : 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whiteMode(MilightThingState state) {
|
||||
sendRepeatableCat(ProtocolConstants.CAT_WHITEMODE, 5, state.colorTemperature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nightMode(MilightThingState state) {
|
||||
sendRepeatableCat(ProtocolConstants.CAT_POWER_MODE, 4, 5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorTemperature(int colorTemp, MilightThingState state) {
|
||||
int ct = (colorTemp * MAX_TEMP) / 100;
|
||||
ct = Math.min(ct, MAX_TEMP);
|
||||
ct = Math.max(ct, 0);
|
||||
sendRepeatableCat(ProtocolConstants.CAT_TEMPERATURE_SET, 5, ct);
|
||||
state.colorTemperature = colorTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getBrCmd() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSaturation(int value, MilightThingState state) {
|
||||
int br = (value * MAX_SAT) / 100; // map value from [0,100] -> [0,MAX_SAT]
|
||||
br = MAX_SAT - br; // inverse value
|
||||
br = Math.min(br, MAX_SAT); // force maximum value
|
||||
br = Math.max(br, 0); // force minimum value
|
||||
sendRepeatableCat(ProtocolConstants.CAT_SATURATION_SET, 2, br);
|
||||
state.saturation = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLedMode(int newmode, MilightThingState state) {
|
||||
int mode = Math.max(Math.min(newmode, 9), 1);
|
||||
sendRepeatableCat(ProtocolConstants.CAT_MODE_SET, 6, mode);
|
||||
state.animationMode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSpeed(int relativeSpeed, MilightThingState state) {
|
||||
sendNonRepeatable(4, relativeSpeed > 1 ? 3 : 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* Implements the iBox led bulb. The bulb is integrated into the iBox and does not include a white channel, so no
|
||||
* saturation or colour temperature available.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightV6RGBIBOXHandler extends AbstractLedV6Handler {
|
||||
private static final byte ADDR = 0x00;
|
||||
|
||||
public MilightV6RGBIBOXHandler(Thing thing, QueuedSend sendQueue) {
|
||||
super(thing, sendQueue, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getAddr() {
|
||||
return ADDR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(boolean on, MilightThingState state) {
|
||||
sendNonRepeatable(3, on ? 3 : 4);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whiteMode(MilightThingState state) {
|
||||
sendRepeatableCat(ProtocolConstants.CAT_WHITEMODE, 3, 5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nightMode(MilightThingState state) {
|
||||
logger.info("Night mode not supported by iBox led!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorTemperature(int colorTemp, MilightThingState state) {
|
||||
logger.info("Color temperature not supported by iBox led!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeColorTemperature(int colorTempRelative, MilightThingState state) {
|
||||
logger.info("Color temperature not supported by iBox led!");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getBrCmd() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSaturation(int value, MilightThingState state) {
|
||||
logger.info("Saturation not supported by iBox led!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLedMode(int newmode, MilightThingState state) {
|
||||
int mode = Math.max(Math.min(newmode, 9), 1);
|
||||
sendRepeatableCat(ProtocolConstants.CAT_MODE_SET, 4, Math.max(Math.min(newmode, 9), 1));
|
||||
state.animationMode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSpeed(int relativeSpeed, MilightThingState state) {
|
||||
sendNonRepeatable(3, relativeSpeed > 1 ? 2 : 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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.milight.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.milight.internal.MilightThingState;
|
||||
import org.openhab.binding.milight.internal.protocol.ProtocolConstants;
|
||||
import org.openhab.binding.milight.internal.protocol.QueuedSend;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* Implements the RGB white bulb. Both leds cannot be on at the same time, so no saturation or colour temperature
|
||||
* control. It still allows more colours than the old v3 rgbw bulb (16320 (255*64) vs 4080 (255*16) colors).
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightV6RGBWHandler extends AbstractLedV6Handler {
|
||||
private static final int ADDR = 0x07;
|
||||
|
||||
public MilightV6RGBWHandler(Thing thing, QueuedSend sendQueue) {
|
||||
super(thing, sendQueue, 20);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getAddr() {
|
||||
return ADDR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPower(boolean on, MilightThingState state) {
|
||||
sendNonRepeatable(3, on ? 1 : 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whiteMode(MilightThingState state) {
|
||||
sendRepeatableCat(ProtocolConstants.CAT_WHITEMODE, 3, 5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nightMode(MilightThingState state) {
|
||||
setPower(true, state);
|
||||
sendRepeatableCat(ProtocolConstants.CAT_POWER_MODE, 3, 6);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorTemperature(int colorTemp, MilightThingState state) {
|
||||
logger.info("Color temperature not supported by RGBW led!");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte getBrCmd() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSaturation(int value, MilightThingState state) {
|
||||
logger.info("Saturation not supported by RGBW led!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLedMode(int newmode, MilightThingState state) {
|
||||
int mode = Math.max(Math.min(newmode, 9), 1);
|
||||
sendRepeatableCat(ProtocolConstants.CAT_MODE_SET, 6, mode);
|
||||
state.animationMode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeSpeed(int relativeSpeed, MilightThingState state) {
|
||||
sendNonRepeatable(4, relativeSpeed > 1 ? 3 : 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
/**
|
||||
* 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.milight.internal.protocol;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The milightV6 protocol is stateful and needs an established session for each client.
|
||||
* This class handles the password bytes, session bytes and sequence number.
|
||||
*
|
||||
* The session handshake is a 3-way handshake. First we are sending either a general
|
||||
* search for bridges command or a search for a specific bridge command (containing the bridge ID)
|
||||
* with our own client session bytes included.
|
||||
*
|
||||
* The response will assign as session bytes that we can use for subsequent commands
|
||||
* see {@link MilightV6SessionManager#sid1} and see {@link MilightV6SessionManager#sid2}.
|
||||
*
|
||||
* We register ourself to the bridge now and finalise the handshake by sending a register command
|
||||
* see {@link MilightV6SessionManager#sendRegistration()} to the bridge.
|
||||
*
|
||||
* From this point on we are required to send keep alive packets to the bridge every ~10sec
|
||||
* to keep the session alive. Because each command we send is confirmed by the bridge, we know if
|
||||
* our session is still valid and can redo the session handshake if necessary.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MilightV6SessionManager implements Runnable, Closeable {
|
||||
protected final Logger logger = LoggerFactory.getLogger(MilightV6SessionManager.class);
|
||||
|
||||
// The used sequence number for a command will be present in the response of the iBox. This
|
||||
// allows us to identify failed command deliveries.
|
||||
private int sequenceNo = 0;
|
||||
|
||||
// Password bytes 1 and 2
|
||||
public byte pw[] = { 0, 0 };
|
||||
|
||||
// Session bytes 1 and 2
|
||||
public byte sid[] = { 0, 0 };
|
||||
|
||||
// Client session bytes 1 and 2. Those are fixed for now.
|
||||
public final byte clientSID1 = (byte) 0xab;
|
||||
public final byte clientSID2 = (byte) 0xde;
|
||||
|
||||
// We need the bridge mac (bridge ID) in many responses to the session commands.
|
||||
private final byte[] bridgeMAC = { (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0 };
|
||||
|
||||
/**
|
||||
* The session handshake is a 3 way handshake.
|
||||
*/
|
||||
public enum SessionState {
|
||||
// No session established and nothing in progress
|
||||
SESSION_INVALID,
|
||||
// Send "find bridge" and wait for response
|
||||
SESSION_WAIT_FOR_BRIDGE,
|
||||
// Send "get session bytes" and wait for response
|
||||
SESSION_WAIT_FOR_SESSION_SID,
|
||||
// Session bytes received, register session now
|
||||
SESSION_NEED_REGISTER,
|
||||
// Registration complete, session is valid now
|
||||
SESSION_VALID,
|
||||
// The session is still active, a keep alive was just received.
|
||||
SESSION_VALID_KEEP_ALIVE,
|
||||
}
|
||||
|
||||
public enum StateMachineInput {
|
||||
NO_INPUT,
|
||||
TIMEOUT,
|
||||
INVALID_COMMAND,
|
||||
KEEP_ALIVE_RECEIVED,
|
||||
BRIDGE_CONFIRMED,
|
||||
SESSION_ID_RECEIVED,
|
||||
SESSION_ESTABLISHED,
|
||||
}
|
||||
|
||||
private SessionState sessionState = SessionState.SESSION_INVALID;
|
||||
|
||||
// Implement this interface to get notifications about the current session state.
|
||||
public interface ISessionState {
|
||||
/**
|
||||
* Notifies about a state change of {@link MilightV6SessionManager}.
|
||||
* SESSION_VALID_KEEP_ALIVE will be reported in the interval, given to the constructor of
|
||||
* {@link MilightV6SessionManager}.
|
||||
*
|
||||
* @param state The new state
|
||||
* @param address The remote IP address. Only guaranteed to be non null in the SESSION_VALID* states.
|
||||
*/
|
||||
void sessionStateChanged(SessionState state, @Nullable InetAddress address);
|
||||
}
|
||||
|
||||
private final ISessionState observer;
|
||||
|
||||
/** Used to determine if the session needs a refresh */
|
||||
private Instant lastSessionConfirmed = Instant.now();
|
||||
/** Quits the receive thread if set to true */
|
||||
private volatile boolean willbeclosed = false;
|
||||
/** Keep track of send commands and their sequence number */
|
||||
private final Map<Integer, Instant> usedSequenceNo = new TreeMap<>();
|
||||
/** The receive thread for all bridge responses. */
|
||||
private final Thread sessionThread;
|
||||
|
||||
private final String bridgeId;
|
||||
private @Nullable DatagramSocket datagramSocket;
|
||||
private @Nullable CompletableFuture<DatagramSocket> startFuture;
|
||||
|
||||
/**
|
||||
* Usually we only send BROADCAST packets. If we know the IP address of the bridge though,
|
||||
* we should try UNICAST packets before falling back to BROADCAST.
|
||||
* This allows communication with the bridge even if it is in another subnet.
|
||||
*/
|
||||
private @Nullable final InetAddress destIP;
|
||||
/**
|
||||
* We cache the last known IP to avoid using broadcast.
|
||||
*/
|
||||
private @Nullable InetAddress lastKnownIP;
|
||||
|
||||
private final int port;
|
||||
|
||||
/** The maximum duration for a session registration / keep alive process in milliseconds. */
|
||||
public static final int TIMEOUT_MS = 10000;
|
||||
/** A packet is handled as lost / not confirmed after this time */
|
||||
public static final int MAX_PACKET_IN_FLIGHT_MS = 2000;
|
||||
/** The keep alive interval. Must be between 100 and REG_TIMEOUT_MS milliseconds or 0 */
|
||||
private final int keepAliveInterval;
|
||||
|
||||
/**
|
||||
* A session manager for the V6 bridge needs a way to send data (a QueuedSend object), the destination bridge ID, a
|
||||
* scheduler for timeout timers and optionally an observer for session state changes.
|
||||
*
|
||||
* @param sendQueue A send queue. Never remove or change that object while the session manager is still working.
|
||||
* @param bridgeId Destination bridge ID. If the bridge ID for whatever reason changes, you need to create a new
|
||||
* session manager object
|
||||
* @param scheduler A framework scheduler to create timeout events.
|
||||
* @param observer Get notifications of state changes
|
||||
* @param destIP If you know the bridge IP address, provide it here.
|
||||
* @param port The bridge port
|
||||
* @param keepAliveInterval The keep alive interval. Must be between 100 and REG_TIMEOUT_MS milliseconds.
|
||||
* if it is equal to REG_TIMEOUT_MS, then a new session will be established instead of renewing the
|
||||
* current one.
|
||||
* @param pw The two "password" bytes for the bridge
|
||||
*/
|
||||
public MilightV6SessionManager(String bridgeId, ISessionState observer, @Nullable InetAddress destIP, int port,
|
||||
int keepAliveInterval, byte[] pw) {
|
||||
this.bridgeId = bridgeId;
|
||||
this.observer = observer;
|
||||
this.destIP = destIP;
|
||||
this.lastKnownIP = destIP;
|
||||
this.port = port;
|
||||
this.keepAliveInterval = keepAliveInterval;
|
||||
this.pw[0] = pw[0];
|
||||
this.pw[1] = pw[1];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
bridgeMAC[i] = Integer.valueOf(bridgeId.substring(i * 2, i * 2 + 2), 16).byteValue();
|
||||
}
|
||||
if (keepAliveInterval < 100 || keepAliveInterval > TIMEOUT_MS) {
|
||||
throw new IllegalArgumentException("keepAliveInterval not within given limits!");
|
||||
}
|
||||
|
||||
sessionThread = new Thread(this, "SessionThread");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the session thread if it is not already running
|
||||
*/
|
||||
public CompletableFuture<DatagramSocket> start() {
|
||||
if (willbeclosed) {
|
||||
CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
|
||||
f.completeExceptionally(null);
|
||||
return f;
|
||||
}
|
||||
if (sessionThread.isAlive()) {
|
||||
DatagramSocket s = datagramSocket;
|
||||
assert s != null;
|
||||
return CompletableFuture.completedFuture(s);
|
||||
}
|
||||
|
||||
CompletableFuture<DatagramSocket> f = new CompletableFuture<>();
|
||||
startFuture = f;
|
||||
sessionThread.start();
|
||||
return f;
|
||||
}
|
||||
|
||||
/**
|
||||
* You have to call that if you are done with this object. Cleans up the receive thread.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (willbeclosed) {
|
||||
return;
|
||||
}
|
||||
willbeclosed = true;
|
||||
final DatagramSocket socket = datagramSocket;
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
sessionThread.interrupt();
|
||||
try {
|
||||
sessionThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
// Set the session id bytes for bridge access. Usually they are acquired automatically
|
||||
// during the session handshake.
|
||||
public void setSessionID(byte[] sid) {
|
||||
this.sid[0] = sid[0];
|
||||
this.sid[1] = sid[1];
|
||||
sessionState = SessionState.SESSION_NEED_REGISTER;
|
||||
}
|
||||
|
||||
// Return the session bytes as hex string
|
||||
public String getSession() {
|
||||
return String.format("%02X %02X", this.sid[0], this.sid[1]);
|
||||
}
|
||||
|
||||
public Instant getLastSessionValidConfirmation() {
|
||||
return lastSessionConfirmed;
|
||||
}
|
||||
|
||||
// Get a new sequence number. Add that to a queue of used sequence numbers.
|
||||
// The bridge response will remove the queued number. This method also checks
|
||||
// for non confirmed sequence numbers older that 2 seconds and report them.
|
||||
public int getNextSequenceNo() {
|
||||
int currentSequenceNo = this.sequenceNo;
|
||||
usedSequenceNo.put(currentSequenceNo, Instant.now());
|
||||
++sequenceNo;
|
||||
return currentSequenceNo;
|
||||
}
|
||||
|
||||
public static byte firstSeqByte(int seq) {
|
||||
return (byte) (seq & 0xff);
|
||||
}
|
||||
|
||||
public static byte secondSeqByte(int seq) {
|
||||
return (byte) ((seq >> 8) & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a search for bridgeID packet on all network interfaces.
|
||||
* This is used for the initial way to determine the IP of the bridge as well
|
||||
* as if the IP of a bridge has changed and the session got invalid because of that.
|
||||
*
|
||||
* A response will assign us session bytes.
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
private void sendSearchForBroadcast(DatagramSocket datagramSocket) {
|
||||
byte[] t = new byte[] { (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0A, (byte) 0x02,
|
||||
clientSID1, clientSID2, (byte) 0x01, bridgeMAC[0], bridgeMAC[1], bridgeMAC[2], bridgeMAC[3],
|
||||
bridgeMAC[4], bridgeMAC[5] };
|
||||
if (lastKnownIP != null) {
|
||||
try {
|
||||
datagramSocket.send(new DatagramPacket(t, t.length, lastKnownIP, port));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not send discover packet! {}", e.getLocalizedMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Enumeration<NetworkInterface> enumNetworkInterfaces;
|
||||
try {
|
||||
enumNetworkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||
} catch (SocketException socketException) {
|
||||
logger.warn("Could not enumerate network interfaces for sending the discover packet!", socketException);
|
||||
return;
|
||||
}
|
||||
DatagramPacket packet = new DatagramPacket(t, t.length, lastKnownIP, port);
|
||||
while (enumNetworkInterfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = enumNetworkInterfaces.nextElement();
|
||||
Iterator<InterfaceAddress> it = networkInterface.getInterfaceAddresses().iterator();
|
||||
while (it.hasNext()) {
|
||||
InterfaceAddress address = it.next();
|
||||
if (address == null) {
|
||||
continue;
|
||||
}
|
||||
InetAddress broadcast = address.getBroadcast();
|
||||
if (broadcast != null && !address.getAddress().isLoopbackAddress()) {
|
||||
packet.setAddress(broadcast);
|
||||
try {
|
||||
datagramSocket.send(packet);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not send discovery packet! {}", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a specific bridge (our bridge). A response will assign us session bytes.
|
||||
// private void send_search_for() {
|
||||
// sendQueue.queue(AbstractBulbInterface.CAT_SESSION, searchForPacket());
|
||||
// }
|
||||
|
||||
private void sendEstablishSession(DatagramSocket datagramSocket) throws IOException {
|
||||
final InetAddress address = lastKnownIP;
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
byte unknown = (byte) 0x1E; // Either checksum or counter. Was 64 and 1e so far.
|
||||
byte[] t = { (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x16, (byte) 0x02, (byte) 0x62,
|
||||
(byte) 0x3A, (byte) 0xD5, (byte) 0xED, (byte) 0xA3, (byte) 0x01, (byte) 0xAE, (byte) 0x08, (byte) 0x2D,
|
||||
(byte) 0x46, (byte) 0x61, (byte) 0x41, (byte) 0xA7, (byte) 0xF6, (byte) 0xDC, (byte) 0xAF, clientSID1,
|
||||
clientSID2, (byte) 0x00, (byte) 0x00, unknown };
|
||||
|
||||
datagramSocket.send(new DatagramPacket(t, t.length, address, port));
|
||||
}
|
||||
|
||||
// Some apps first send {@see send_establish_session} and with the aquired session bytes they
|
||||
// subsequently send this command for establishing the session. This is not well documented unfortunately.
|
||||
@SuppressWarnings("unused")
|
||||
private void sendPreRegistration(DatagramSocket datagramSocket) throws IOException {
|
||||
final InetAddress address = lastKnownIP;
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
byte[] t = { 0x30, 0, 0, 0, 3, sid[0], sid[1], 1, 0 };
|
||||
datagramSocket.send(new DatagramPacket(t, t.length, address, port));
|
||||
}
|
||||
|
||||
// After the bridges knows our client session bytes and we know the bridge session bytes, we do a final
|
||||
// registration with this command. The response will again contain the bridge ID and the session should
|
||||
// be established by then.
|
||||
private void sendRegistration(DatagramSocket datagramSocket) throws IOException {
|
||||
final InetAddress address = lastKnownIP;
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int seq = getNextSequenceNo();
|
||||
byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], firstSeqByte(seq), secondSeqByte(seq), 0x00,
|
||||
0x33, pw[0], pw[1], 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) (0x33 + pw[0] + pw[1]) };
|
||||
datagramSocket.send(new DatagramPacket(t, t.length, address, port));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a 0x80... command which us used for all colour,brightness,saturation,mode operations.
|
||||
* The session ID, password and sequence number is automatically inserted from this object.
|
||||
*
|
||||
* Produces data like:
|
||||
* SN: Sequence number
|
||||
* S1: SessionID1
|
||||
* S2: SessionID2
|
||||
* P1/P2: Password bytes
|
||||
* WB: Remote (08) or iBox integrated bulb (00)
|
||||
* ZN: Zone {Zone1-4 0=All}
|
||||
* CK: Checksum
|
||||
*
|
||||
* #zone 1 on
|
||||
* @ 80 00 00 00 11 84 00 00 0c 00 31 00 00 08 04 01 00 00 00 01 00 3f
|
||||
*
|
||||
* Colors:
|
||||
* CC: Color value (hue)
|
||||
* 80 00 00 00 11 S1 S2 SN SN 00 31 P1 P2 WB 01 CC CC CC CC ZN 00 CK
|
||||
*
|
||||
* 80 00 00 00 11 D4 00 00 12 00 31 00 00 08 01 FF FF FF FF 01 00 38
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public byte[] makeCommand(byte wb, int zone, int... data) {
|
||||
int seq = getNextSequenceNo();
|
||||
byte[] t = { (byte) 0x80, 0x00, 0x00, 0x00, 0x11, sid[0], sid[1], MilightV6SessionManager.firstSeqByte(seq),
|
||||
MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31, pw[0], pw[1], wb, 0, 0, 0, 0, 0, (byte) zone, 0,
|
||||
0 };
|
||||
|
||||
for (int i = 0; i < data.length; ++i) {
|
||||
t[14 + i] = (byte) data[i];
|
||||
}
|
||||
|
||||
byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
|
||||
+ t[10 + 7] + t[10 + 8] + zone);
|
||||
t[21] = chksum;
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a 0x3D or 0x3E link/unlink command.
|
||||
* The session ID, password and sequence number is automatically inserted from this object.
|
||||
*
|
||||
* WB: Remote (08) or iBox integrated bulb (00)
|
||||
*/
|
||||
public byte[] makeLink(byte wb, int zone, boolean link) {
|
||||
int seq = getNextSequenceNo();
|
||||
byte[] t = { (link ? (byte) 0x3D : (byte) 0x3E), 0x00, 0x00, 0x00, 0x11, sid[0], sid[1],
|
||||
MilightV6SessionManager.firstSeqByte(seq), MilightV6SessionManager.secondSeqByte(seq), 0x00, 0x31,
|
||||
pw[0], pw[1], wb, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) zone, 0x00, 0x00 };
|
||||
|
||||
byte chksum = (byte) (t[10 + 0] + t[10 + 1] + t[10 + 2] + t[10 + 3] + t[10 + 4] + t[10 + 5] + t[10 + 6]
|
||||
+ t[10 + 7] + t[10 + 8] + zone);
|
||||
t[21] = chksum;
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main state machine of the session handshake.
|
||||
*
|
||||
* @throws InterruptedException
|
||||
* @throws IOException
|
||||
*/
|
||||
private void sessionStateMachine(DatagramSocket datagramSocket, StateMachineInput input) throws IOException {
|
||||
final SessionState lastSessionState = sessionState;
|
||||
|
||||
// Check for timeout
|
||||
final Instant current = Instant.now();
|
||||
final Duration timeElapsed = Duration.between(lastSessionConfirmed, current);
|
||||
if (timeElapsed.toMillis() > TIMEOUT_MS) {
|
||||
if (sessionState != SessionState.SESSION_WAIT_FOR_BRIDGE) {
|
||||
logger.warn("Session timeout!");
|
||||
}
|
||||
// One reason we failed, might be that a last known IP is not correct anymore.
|
||||
// Reset to the given dest IP (which might be null).
|
||||
lastKnownIP = destIP;
|
||||
sessionState = SessionState.SESSION_INVALID;
|
||||
}
|
||||
|
||||
if (input == StateMachineInput.INVALID_COMMAND) {
|
||||
sessionState = SessionState.SESSION_INVALID;
|
||||
}
|
||||
|
||||
// Check old seq no:
|
||||
for (Iterator<Map.Entry<Integer, Instant>> it = usedSequenceNo.entrySet().iterator(); it.hasNext();) {
|
||||
Map.Entry<Integer, Instant> entry = it.next();
|
||||
if (Duration.between(entry.getValue(), current).toMillis() > MAX_PACKET_IN_FLIGHT_MS) {
|
||||
logger.debug("Command not confirmed: {}", entry.getKey());
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
switch (sessionState) {
|
||||
case SESSION_INVALID:
|
||||
usedSequenceNo.clear();
|
||||
sessionState = SessionState.SESSION_WAIT_FOR_BRIDGE;
|
||||
lastSessionConfirmed = Instant.now();
|
||||
case SESSION_WAIT_FOR_BRIDGE:
|
||||
if (input == StateMachineInput.BRIDGE_CONFIRMED) {
|
||||
sessionState = SessionState.SESSION_WAIT_FOR_SESSION_SID;
|
||||
} else {
|
||||
datagramSocket.setSoTimeout(150);
|
||||
sendSearchForBroadcast(datagramSocket);
|
||||
break;
|
||||
}
|
||||
case SESSION_WAIT_FOR_SESSION_SID:
|
||||
if (input == StateMachineInput.SESSION_ID_RECEIVED) {
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
logger.debug("Session ID received: {}", String.format("%02X %02X", this.sid[0], this.sid[1]));
|
||||
}
|
||||
sessionState = SessionState.SESSION_NEED_REGISTER;
|
||||
} else {
|
||||
datagramSocket.setSoTimeout(300);
|
||||
sendEstablishSession(datagramSocket);
|
||||
break;
|
||||
}
|
||||
case SESSION_NEED_REGISTER:
|
||||
if (input == StateMachineInput.SESSION_ESTABLISHED) {
|
||||
sessionState = SessionState.SESSION_VALID;
|
||||
lastSessionConfirmed = Instant.now();
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
logger.debug("Registration complete");
|
||||
}
|
||||
} else {
|
||||
datagramSocket.setSoTimeout(300);
|
||||
sendRegistration(datagramSocket);
|
||||
break;
|
||||
}
|
||||
case SESSION_VALID_KEEP_ALIVE:
|
||||
case SESSION_VALID:
|
||||
if (input == StateMachineInput.KEEP_ALIVE_RECEIVED) {
|
||||
lastSessionConfirmed = Instant.now();
|
||||
observer.sessionStateChanged(SessionState.SESSION_VALID_KEEP_ALIVE, lastKnownIP);
|
||||
} else {
|
||||
final InetAddress address = lastKnownIP;
|
||||
if (keepAliveInterval > 0 && timeElapsed.toMillis() > keepAliveInterval && address != null) {
|
||||
// Send keep alive
|
||||
byte[] t = { (byte) 0xD0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, sid[0], sid[1] };
|
||||
datagramSocket.send(new DatagramPacket(t, t.length, address, port));
|
||||
}
|
||||
// Increase socket timeout to wake up for the next keep alive interval
|
||||
datagramSocket.setSoTimeout(keepAliveInterval);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (lastSessionState != sessionState) {
|
||||
observer.sessionStateChanged(sessionState, lastKnownIP);
|
||||
}
|
||||
}
|
||||
|
||||
private void logUnknownPacket(byte[] data, int len, String reason) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int i = 0; i < len; ++i) {
|
||||
s.append(String.format("%02X ", data[i]));
|
||||
}
|
||||
s.append("Sid: ");
|
||||
s.append(String.format("%02X ", clientSID1));
|
||||
s.append(String.format("%02X ", clientSID2));
|
||||
logger.info("{} ({}): {}", reason, bridgeId, s);
|
||||
}
|
||||
|
||||
/**
|
||||
* The session thread executes this run() method and a blocking UDP receive
|
||||
* is performed in a loop.
|
||||
*/
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@Override
|
||||
public void run() {
|
||||
try (DatagramSocket datagramSocket = new DatagramSocket(null)) {
|
||||
this.datagramSocket = datagramSocket;
|
||||
datagramSocket.setBroadcast(true);
|
||||
datagramSocket.setReuseAddress(true);
|
||||
datagramSocket.setSoTimeout(150);
|
||||
datagramSocket.bind(null);
|
||||
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
logger.debug("MilightCommunicationV6 receive thread ready");
|
||||
}
|
||||
|
||||
// Inform the start future about the datagram socket
|
||||
CompletableFuture<DatagramSocket> f = startFuture;
|
||||
if (f != null) {
|
||||
f.complete(datagramSocket);
|
||||
startFuture = null;
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
|
||||
|
||||
sessionStateMachine(datagramSocket, StateMachineInput.NO_INPUT);
|
||||
|
||||
// Now loop forever, waiting to receive packets and printing them.
|
||||
while (!willbeclosed) {
|
||||
rPacket.setLength(buffer.length);
|
||||
try {
|
||||
datagramSocket.receive(rPacket);
|
||||
} catch (SocketTimeoutException e) {
|
||||
sessionStateMachine(datagramSocket, StateMachineInput.TIMEOUT);
|
||||
continue;
|
||||
}
|
||||
int len = rPacket.getLength();
|
||||
|
||||
if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
|
||||
logUnknownPacket(buffer, len, "Not an iBox response!");
|
||||
continue;
|
||||
}
|
||||
|
||||
int expectedLen = buffer[4] + 5;
|
||||
|
||||
if (expectedLen > len) {
|
||||
logUnknownPacket(buffer, len, "Unexpected size!");
|
||||
continue;
|
||||
}
|
||||
switch (buffer[0]) {
|
||||
// 13 00 00 00 0A 03 D3 54 11 (AC CF 23 F5 7A D4)
|
||||
case (byte) 0x13: {
|
||||
boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 9, 6));
|
||||
if (eq) {
|
||||
logger.debug("TODO: Feedback required");
|
||||
// I have no clue what that packet means. But the bridge is going to timeout the next
|
||||
// keep alive and it is a good idea to start the session again.
|
||||
} else {
|
||||
logger.info("Unknown 0x13 received, but not for our bridge ({})", bridgeId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// 18 00 00 00 40 02 (AC CF 23 F5 7A D4) 00 20 39 38 35 62 31 35 37 62 66 36 66 63 34 33 33 36 38 61
|
||||
// 36 33 34 36 37 65 61 33 62 31 39 64 30 64 01 00 01 17 63 00 00 05 00 09 78 6C 69 6E 6B 5F 64 65
|
||||
// 76 07 5B CD 15
|
||||
// ASCII string contained: 985b157bf6fc43368a63467ea3b19d0dc .. xlink_dev
|
||||
// Response to the v6 SEARCH and the SEARCH FOR commands to look for new or known devices.
|
||||
// Our session id will be transfered in this process (!= bridge session id)
|
||||
case (byte) 0x18: {
|
||||
boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 6, 6));
|
||||
if (eq) {
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
logger.debug("Session ID reestablished");
|
||||
}
|
||||
lastKnownIP = rPacket.getAddress();
|
||||
sessionStateMachine(datagramSocket, StateMachineInput.BRIDGE_CONFIRMED);
|
||||
} else {
|
||||
logger.info("Session ID received, but not for our bridge ({})", bridgeId);
|
||||
logUnknownPacket(buffer, len, "ID not matching");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// 28 00 00 00 11 00 02 (AC CF 23 F5 7A D4) 50 AA 4D 2A 00 01 SS_ID 00
|
||||
// Response to the keepAlive() packet if session is not valid yet.
|
||||
// Should contain the session ids
|
||||
case (byte) 0x28: {
|
||||
boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 7, 6));
|
||||
if (eq) {
|
||||
this.sid[0] = buffer[19];
|
||||
this.sid[1] = buffer[20];
|
||||
sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ID_RECEIVED);
|
||||
} else {
|
||||
logger.info("Session ID received, but not for our bridge ({})", bridgeId);
|
||||
logUnknownPacket(buffer, len, "ID not matching");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// 80 00 00 00 15 (AC CF 23 F5 7A D4) 05 02 00 34 00 00 00 00 00 00 00 00 00 00 34
|
||||
// Response to the registration packet
|
||||
case (byte) 0x80: {
|
||||
boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
|
||||
if (eq) {
|
||||
sessionStateMachine(datagramSocket, StateMachineInput.SESSION_ESTABLISHED);
|
||||
} else {
|
||||
logger.info("Registration received, but not for our bridge ({})", bridgeId);
|
||||
logUnknownPacket(buffer, len, "ID not matching");
|
||||
}
|
||||
break;
|
||||
}
|
||||
// 88 00 00 00 03 SN SN OK // two byte sequence number, we use the later one only.
|
||||
// OK: is 00 if ok or 01 if failed
|
||||
case (byte) 0x88:
|
||||
int seq = Byte.toUnsignedInt(buffer[6]) + Byte.toUnsignedInt(buffer[7]) * 256;
|
||||
Instant timePacketWasSend = usedSequenceNo.remove(seq);
|
||||
if (timePacketWasSend != null) {
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
logger.debug("Confirmation received for command: {}", String.valueOf(seq));
|
||||
}
|
||||
if (buffer[8] == 1) {
|
||||
logger.warn("Command {} failed", seq);
|
||||
}
|
||||
} else {
|
||||
// another participant might have established a session from the same host
|
||||
logger.info("Confirmation received for unsend command. Sequence number: {}",
|
||||
String.valueOf(seq));
|
||||
}
|
||||
break;
|
||||
// D8 00 00 00 07 (AC CF 23 F5 7A D4) 01
|
||||
// Response to the keepAlive() packet
|
||||
case (byte) 0xD8: {
|
||||
boolean eq = ByteBuffer.wrap(bridgeMAC, 0, 6).equals(ByteBuffer.wrap(buffer, 5, 6));
|
||||
if (eq) {
|
||||
sessionStateMachine(datagramSocket, StateMachineInput.KEEP_ALIVE_RECEIVED);
|
||||
} else {
|
||||
logger.info("Keep alive received but not for our bridge ({})", bridgeId);
|
||||
logUnknownPacket(buffer, len, "ID not matching");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logUnknownPacket(buffer, len, "No valid start byte");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (!willbeclosed) {
|
||||
logger.warn("Session Manager receive thread failed: {}", e.getLocalizedMessage(), e);
|
||||
}
|
||||
} finally {
|
||||
this.datagramSocket = null;
|
||||
}
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
logger.debug("MilightCommunicationV6 receive thread stopped");
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the session is established successfully
|
||||
public boolean isValid() {
|
||||
return sessionState == SessionState.SESSION_VALID;
|
||||
}
|
||||
}
|
||||
@@ -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.milight.internal.protocol;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Implement this bulb interface for each new bulb type. It is used by {@see MilightLedHandler} to handle commands.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class ProtocolConstants {
|
||||
// Print out a lot of useful debug data for the session establishing
|
||||
// This is for development purposes only, if the protocol changes again.
|
||||
public static final boolean DEBUG_SESSION = true;
|
||||
|
||||
/**
|
||||
* There can only be one command of a category in the send queue (to avoid
|
||||
* having multiple on/off commands in the queue for example). You can assign
|
||||
* a category to each command you send and use one of the following constants.
|
||||
*/
|
||||
// Session commands
|
||||
public static final int CAT_DISCOVER = 1;
|
||||
|
||||
// Bulb commands
|
||||
public static final int CAT_BRIGHTNESS_SET = 10;
|
||||
public static final int CAT_SATURATION_SET = 11;
|
||||
public static final int CAT_COLOR_SET = 12;
|
||||
public static final int CAT_POWER_MODE = 13;
|
||||
public static final int CAT_TEMPERATURE_SET = 14;
|
||||
public static final int CAT_WHITEMODE = 17;
|
||||
public static final int CAT_MODE_SET = 18;
|
||||
public static final int CAT_SPEED_CHANGE = 19;
|
||||
public static final int CAT_LINK = 20;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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.milight.internal.protocol;
|
||||
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A queue item meant to be used for {@link QueuedSend}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class QueueItem {
|
||||
protected final int uniqueCommandId;
|
||||
protected final boolean repeatable;
|
||||
protected final int delayTime;
|
||||
protected final int repeatCommands;
|
||||
protected final DatagramPacket packet;
|
||||
private boolean invalid = false;
|
||||
|
||||
final DatagramSocket socket;
|
||||
private final QueueItem root;
|
||||
private @Nullable QueueItem last;
|
||||
public @Nullable QueueItem next;
|
||||
|
||||
/**
|
||||
* Add data to the send queue.
|
||||
* Commands which need to be queued up and not replacing same type commands must be non-categorised.
|
||||
* Items can be repeatable, in the sense that they do not cause side effects if they are send multiple times
|
||||
* (e.g. absolute values).
|
||||
*
|
||||
* @param socket A socket
|
||||
* @param uniqueCommandId A unique command id. A later command with the same id as previous ones will
|
||||
* overwrite those. 0 means a non categorised entry.
|
||||
* @param data Data to be send
|
||||
* @param repeatable A repeatable command should not cause side effects by sending it multiple times.
|
||||
* @param delayTime A delay time that is used instead of the default delay time between commands for all
|
||||
* added byte sequences.
|
||||
*/
|
||||
public QueueItem(DatagramSocket socket, int uniqueCommandId, byte[] data, boolean repeatable, int delayTime,
|
||||
int repeatCommands, InetAddress address, int port) {
|
||||
this.socket = socket;
|
||||
this.uniqueCommandId = uniqueCommandId;
|
||||
this.repeatable = repeatable;
|
||||
this.delayTime = delayTime;
|
||||
this.repeatCommands = repeatCommands;
|
||||
this.root = this;
|
||||
this.packet = new DatagramPacket(data, data.length, address, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #QueueItem(int, byte[], boolean, int)
|
||||
* @param uniqueCommandId A unique command id. A later command with the same id as previous ones will
|
||||
* overwrite those. 0 means a non categorised entry.
|
||||
* @param data Data to be send
|
||||
* @param repeatable A repeatable command should not cause side effects by sending it multiple times.
|
||||
* @param customDelayTime A delay time that is used instead of the default delay time between commands for all
|
||||
* added byte sequences.
|
||||
* @param root Another item is the root entry for this one.
|
||||
*/
|
||||
private QueueItem(DatagramSocket socket, int uniqueCommandId, byte[] data, boolean repeatable, int customDelayTime,
|
||||
int repeatCommands, InetAddress address, int port, QueueItem root) {
|
||||
this.socket = socket;
|
||||
this.uniqueCommandId = uniqueCommandId;
|
||||
this.repeatable = repeatable;
|
||||
this.delayTime = customDelayTime;
|
||||
this.repeatCommands = repeatCommands;
|
||||
this.root = root;
|
||||
this.packet = new DatagramPacket(data, data.length, address, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add non-categorised, repeatable data to the send queue.
|
||||
*
|
||||
* <p>
|
||||
* Commands which need to be queued up and not replacing same type commands must use this method.
|
||||
* Items added to the queue are considered not repeatable (suited for relative commands where a repetition would
|
||||
* cause a change of value).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* If you want to, you can add multiple byte sequences to the queue, even if they have the same id.
|
||||
* This is used for animations and multi-message commands. Be aware that a later added command with the
|
||||
* same id will replace all those commands at once.
|
||||
* </p>
|
||||
*
|
||||
* @param data Data to be send
|
||||
*/
|
||||
public static QueueItem createRepeatable(DatagramSocket socket, int customDelayTime, int repeatCommands,
|
||||
InetAddress address, int port, byte[]... data) {
|
||||
QueueItem item = new QueueItem(socket, QueuedSend.NO_CATEGORY, data[0], true, customDelayTime, repeatCommands,
|
||||
address, port);
|
||||
|
||||
QueueItem next = item;
|
||||
for (int i = 1; i < data.length; ++i) {
|
||||
next = next.addRepeatable(data[i]);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add non-categorised, non-repeatable data to the send queue.
|
||||
*
|
||||
* <p>
|
||||
* Commands which need to be queued up and not replacing same type commands must use this method.
|
||||
* Items added to the queue are considered not repeatable (suited for relative commands where a repetition would
|
||||
* cause a change of value).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* If you want to, you can add multiple byte sequences to the queue, even if they have the same id.
|
||||
* This is used for animations and multi-message commands. Be aware that a later added command with the
|
||||
* same id will replace all those commands at once.
|
||||
* </p>
|
||||
*
|
||||
* @param data Data to be send
|
||||
*/
|
||||
public static QueueItem createNonRepeatable(DatagramSocket socket, int customDelayTime, InetAddress address,
|
||||
int port, byte[]... data) {
|
||||
QueueItem item = new QueueItem(socket, QueuedSend.NO_CATEGORY, data[0], false, customDelayTime, 1, address,
|
||||
port);
|
||||
|
||||
QueueItem next = item;
|
||||
for (int i = 1; i < data.length; ++i) {
|
||||
next = next.addRepeatable(data[i]);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private QueueItem createNewItem(byte[] data, boolean repeatable) {
|
||||
QueueItem newItem = new QueueItem(socket, uniqueCommandId, data, repeatable, delayTime, repeatCommands,
|
||||
packet.getAddress(), packet.getPort(), root);
|
||||
|
||||
// The the next pointer of the last element in the chain to the new item
|
||||
QueueItem lastInChain = root.last != null ? root.last : root;
|
||||
lastInChain.next = newItem;
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a command to the command chain. Can be called on any of the
|
||||
* commands in the chain, but will overwrite the next command in chain.
|
||||
*
|
||||
* This method can be used in a cascading way like:
|
||||
* QueueItem item ...;
|
||||
* send(item.addNonRepeatable(...).addNonRepeatable(...))
|
||||
*
|
||||
*
|
||||
* @param data Add data to the chain of commands
|
||||
* @return Always return the root command
|
||||
*/
|
||||
public QueueItem addNonRepeatable(byte[] data) {
|
||||
root.last = createNewItem(data, false);
|
||||
return this.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a command to the command chain. Can be called on any of the
|
||||
* commands in the chain, but will overwrite the next command in chain.
|
||||
*
|
||||
* This method can be used in a cascading way like:
|
||||
* QueueItem item ...;
|
||||
* send(item.addRepeatable(...).addRepeatable(...))
|
||||
*
|
||||
*
|
||||
* @param data Add data to the chain of commands
|
||||
* @return Always return the root command
|
||||
*/
|
||||
public QueueItem addRepeatable(byte[] data) {
|
||||
root.last = createNewItem(data, true);
|
||||
return this.root;
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return invalid;
|
||||
}
|
||||
|
||||
public void makeInvalid() {
|
||||
this.invalid = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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.milight.internal.protocol;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This implements a queue for UDP sending, where each item to be send is associated with an id.
|
||||
* If a new item is added, that has the same id of an already queued item, it replaces the
|
||||
* queued item. This is used for milight packets, where older bridges accept commands with a 100ms
|
||||
* delay only. The user may issue absolute brightness or color changes faster than 1/10s though, and we don't
|
||||
* want to just queue up those commands but apply the newest command only.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class QueuedSend implements Runnable, Closeable {
|
||||
private final Logger logger = LoggerFactory.getLogger(QueuedSend.class);
|
||||
|
||||
final BlockingQueue<QueueItem> queue = new LinkedBlockingQueue<>(20);
|
||||
private boolean willbeclosed = false;
|
||||
private @Nullable Thread thread;
|
||||
|
||||
public static final byte NO_CATEGORY = 0;
|
||||
|
||||
/**
|
||||
* Start the send thread of this queue. Call dispose() to quit the thread.
|
||||
*/
|
||||
public void start() {
|
||||
willbeclosed = false;
|
||||
thread = new Thread(this);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* The queue process
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
QueueItem item = null;
|
||||
while (!willbeclosed) {
|
||||
// If the command belongs to a chain of commands, get the next command now.
|
||||
if (item != null && item.next != null) {
|
||||
item = item.next;
|
||||
} else {
|
||||
try {
|
||||
// block/wait for another item
|
||||
item = queue.take();
|
||||
} catch (InterruptedException e) {
|
||||
if (!willbeclosed) {
|
||||
logger.error("Queue take failed: {}", e.getLocalizedMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.isInvalid()) {
|
||||
// Just in case it is a command chain, set the item to null to not process any chained commands.
|
||||
item = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < (item.repeatable ? item.repeatCommands : 1); ++i) {
|
||||
item.socket.send(item.packet);
|
||||
|
||||
if (ProtocolConstants.DEBUG_SESSION) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int c = 0; c < item.packet.getData().length; ++c) {
|
||||
s.append(String.format("%02X ", item.packet.getData()[c]));
|
||||
}
|
||||
logger.debug("Sent packet '{}' to bridge {}", s.toString(),
|
||||
item.packet.getAddress().getHostAddress());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to send Message to '{}': {}", item.packet.getAddress().getHostAddress(),
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(item.delayTime);
|
||||
} catch (InterruptedException e) {
|
||||
if (!willbeclosed) {
|
||||
logger.warn("Queue sleep failed: {}", e.getLocalizedMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all commands in the queue invalid that have the same unique id as the given one. This does not synchronise
|
||||
* with the sender thread. If an element has been started to being processed, this method has no more effect on that
|
||||
* element. Command chains are always executed in a row. Even if the head of the command queue has been marked
|
||||
* as invalid, if the processing has been started, the chain will be processed completely.
|
||||
*
|
||||
* @param uniqueCommandId
|
||||
*/
|
||||
private void removeFromQueue(int uniqueCommandId) {
|
||||
Iterator<QueueItem> iterator = queue.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
try {
|
||||
QueueItem item = iterator.next();
|
||||
if (item.uniqueCommandId == uniqueCommandId) {
|
||||
item.makeInvalid();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Ignore threading errors
|
||||
} catch (NoSuchElementException e) {
|
||||
// The element might have been processed already while iterate.
|
||||
// Ignore NoSuchElementException here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to the send queue.
|
||||
*
|
||||
* <p>
|
||||
* You have to create your own QueueItem. This allows to you create a chain of commands. A chain will always
|
||||
* executed in order and without interrupting the sequence with another command. A chain will be removed completely
|
||||
* if another command with the same category is added except if the chain has been started to be processed.
|
||||
* </p>
|
||||
*
|
||||
* @param item A queue item, cannot be null.
|
||||
*/
|
||||
public void queue(QueueItem item) {
|
||||
if (item.uniqueCommandId != NO_CATEGORY) {
|
||||
removeFromQueue(item.uniqueCommandId);
|
||||
}
|
||||
queue.offer(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Once closed, this object can't be reused anymore.
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
willbeclosed = true;
|
||||
final Thread threadL = this.thread;
|
||||
if (threadL != null) {
|
||||
try {
|
||||
threadL.join(200);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
threadL.interrupt();
|
||||
}
|
||||
this.thread = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* 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.milight.internal.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Emulates a Milight V6 iBox bridge to test and intercept communication with the official apps,
|
||||
* as well as test the binding to be conformant to the protocol.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class EmulatedV6Bridge {
|
||||
protected final Logger logger = LoggerFactory.getLogger(EmulatedV6Bridge.class);
|
||||
private boolean willbeclosed = false;
|
||||
private static final byte SID1 = (byte) 0xed;
|
||||
private static final byte SID2 = (byte) 0xab;
|
||||
private static final byte PW1 = 0;
|
||||
private static final byte PW2 = 0;
|
||||
|
||||
// These bytes are the client session bytes
|
||||
private byte cls1 = (byte) 0xf6;
|
||||
private byte cls2 = (byte) 0x0D;
|
||||
|
||||
private static final byte SEQ1 = 0, SEQ2 = 0;
|
||||
|
||||
private static final byte[] FAKE_MAC = { (byte) 0xAC, (byte) 0xCF, (byte) 0x23, (byte) 0xF5, (byte) 0x7A,
|
||||
(byte) 0xD4 };
|
||||
|
||||
// Send to the network by clients to find V6 bridges
|
||||
private byte searchBroadcast[] = new byte[] { 0x10, 0, 0, 0, 0x24, 0x02, cls1, cls2, 0x02, 0x39, 0x38, 0x35, 0x62,
|
||||
0x31, 0x35, 0x37, 0x62, 0x66, 0x36, 0x66, 0x63, 0x34, 0x33, 0x33, 0x36, 0x38, 0x61, 0x36, 0x33, 0x34, 0x36,
|
||||
0x37, 0x65, 0x61, 0x33, 0x62, 0x31, 0x39, 0x64, 0x30, 0x64 };
|
||||
|
||||
// Send to broadcast address by the client usually and used to test if the client with the contained bridge id
|
||||
// is present on the network. If the IP of the bridge is known already, then SESSION_REQUEST is used usually.
|
||||
private byte sessionRequestFindBroadcast[] = new byte[] { 0x10, 0, 0, 0, 0x0A, 2, cls1, cls2, 1, FAKE_MAC[0],
|
||||
FAKE_MAC[1], FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5] };
|
||||
|
||||
// Some clients send this as first command to get a session id, especially if the bridge IP is already known.
|
||||
private byte sessionRequest[] = new byte[] { (byte) 0x20, 0, 0, 0, (byte) 0x16, 2, (byte) 0x62, (byte) 0x3A,
|
||||
(byte) 0xD5, (byte) 0xED, (byte) 0xA3, 1, (byte) 0xAE, (byte) 0x08, (byte) 0x2D, (byte) 0x46, (byte) 0x61,
|
||||
(byte) 0x41, (byte) 0xA7, (byte) 0xF6, (byte) 0xDC, (byte) 0xAF, cls1, cls2, 0, 0, (byte) 0x1E };
|
||||
|
||||
private byte sessionResponse[] = { (byte) 0x28, 0, 0, 0, (byte) 0x11, 0, 2, (byte) 0xAC, (byte) 0xCF, (byte) 0x23,
|
||||
(byte) 0xF5, (byte) 0x7A, (byte) 0xD4, (byte) 0x69, (byte) 0xF0, (byte) 0x3C, (byte) 0x23, 0, 1, SID1, SID2,
|
||||
0 };
|
||||
|
||||
// Some clients call this as second command to establish a session.
|
||||
private static final byte ESTABLISH_SESSION_REQUEST[] = new byte[] { (byte) 0x30, 0, 0, 0, 3, SID1, SID2, 0 };
|
||||
|
||||
// In response to SEARCH, ESTABLISH_SESSION_REQUEST but also to SESSION_REQUEST_FIND_BROADCAST
|
||||
private static final byte REESTABLISH_SESSION_RESPONSE[] = new byte[] { (byte) 0x18, 0, 0, 0, (byte) 0x40, 2,
|
||||
FAKE_MAC[0], FAKE_MAC[1], FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 0, (byte) 0x20, (byte) 0x39,
|
||||
(byte) 0x38, (byte) 0x35, (byte) 0x62, (byte) 0x31, (byte) 0x35, (byte) 0x37, (byte) 0x62, (byte) 0x66,
|
||||
(byte) 0x36, (byte) 0x66, (byte) 0x63, (byte) 0x34, (byte) 0x33, (byte) 0x33, (byte) 0x36, (byte) 0x38,
|
||||
(byte) 0x61, (byte) 0x36, (byte) 0x33, (byte) 0x34, (byte) 0x36, (byte) 0x37, (byte) 0x65, (byte) 0x61,
|
||||
(byte) 0x33, (byte) 0x62, (byte) 0x31, (byte) 0x39, (byte) 0x64, (byte) 0x30, (byte) 0x64, 1, 0, 1,
|
||||
(byte) 0x17, (byte) 0x63, 0, 0, (byte) 0x05, 0, (byte) 0x09, (byte) 0x78, (byte) 0x6C, (byte) 0x69,
|
||||
(byte) 0x6E, (byte) 0x6B, (byte) 0x5F, (byte) 0x64, (byte) 0x65, (byte) 0x76, (byte) 0x07, (byte) 0x5B,
|
||||
(byte) 0xCD, (byte) 0x15 };
|
||||
|
||||
private static final byte REGISTRATION_REQUEST[] = { (byte) 0x80, 0, 0, 0, 0x11, SID1, SID2, SEQ1, SEQ2, 0, 0x33,
|
||||
PW1, PW2, 0, 0, 0, 0, 0, 0, 0, 0, 0x33 };
|
||||
|
||||
// 80:00:00:00:15:(f0:fe:6b:16:b0:8a):05:02:00:34:00:00:00:00:00:00:00:00:00:00:34
|
||||
private static final byte REGISTRATION_REQUEST_RESPONSE[] = { (byte) 0x80, 0, 0, 0, 0x15, FAKE_MAC[0], FAKE_MAC[1],
|
||||
FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 5, 2, 0, 0x34, PW1, PW2, 0, 0, 0, 0, 0, 0, 0, 0, 0x34 };
|
||||
|
||||
private static final byte[] KEEP_ALIVE_REQUEST = { (byte) 0xD0, 0, 0, 0, 2, SID1, SID2 };
|
||||
|
||||
private static final byte[] KEEP_ALIVE_RESPONSE = { (byte) 0xD8, 0, 0, 0, (byte) 0x07, FAKE_MAC[0], FAKE_MAC[1],
|
||||
FAKE_MAC[2], FAKE_MAC[3], FAKE_MAC[4], FAKE_MAC[5], 1 };
|
||||
|
||||
EmulatedV6Bridge() {
|
||||
new Thread(this::runDiscovery).start();
|
||||
new Thread(this::runBrigde).start();
|
||||
}
|
||||
|
||||
private void replaceWithMac(byte data[], int offset) {
|
||||
data[offset + 0] = FAKE_MAC[0];
|
||||
data[offset + 1] = FAKE_MAC[1];
|
||||
data[offset + 2] = FAKE_MAC[2];
|
||||
data[offset + 3] = FAKE_MAC[3];
|
||||
data[offset + 4] = FAKE_MAC[4];
|
||||
data[offset + 5] = FAKE_MAC[5];
|
||||
}
|
||||
|
||||
public void runDiscovery() {
|
||||
final byte discover[] = "HF-A11ASSISTHREAD".getBytes();
|
||||
|
||||
try {
|
||||
byte[] a = new byte[0];
|
||||
DatagramPacket sPacket = new DatagramPacket(a, a.length);
|
||||
DatagramSocket datagramSocket = new DatagramSocket(MilightBindingConstants.PORT_DISCOVER);
|
||||
|
||||
debugSession("EmulatedV6Bridge discover thread ready");
|
||||
byte[] buffer = new byte[1024];
|
||||
DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
|
||||
|
||||
// Now loop forever, waiting to receive packets and printing them.
|
||||
while (!willbeclosed) {
|
||||
rPacket.setLength(buffer.length);
|
||||
datagramSocket.receive(rPacket);
|
||||
sPacket.setAddress(rPacket.getAddress());
|
||||
sPacket.setPort(rPacket.getPort());
|
||||
|
||||
int len = rPacket.getLength();
|
||||
|
||||
if (len >= discover.length) {
|
||||
if (Arrays.equals(discover, Arrays.copyOf(buffer, discover.length))) {
|
||||
String data = rPacket.getAddress().getHostAddress() + ",ACCF23F57AD4,HF-LPB100";
|
||||
debugSession("Discover message received. Send: " + data);
|
||||
sendMessage(sPacket, datagramSocket, data.getBytes());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// logUnknownPacket(buffer, len, "No valid discovery received");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (willbeclosed) {
|
||||
return;
|
||||
}
|
||||
logger.error("{}", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void runBrigde() {
|
||||
try {
|
||||
byte[] a = new byte[0];
|
||||
DatagramPacket sPacket = new DatagramPacket(a, a.length);
|
||||
DatagramSocket datagramSocket = new DatagramSocket(MilightBindingConstants.PORT_VER6);
|
||||
|
||||
debugSession("EmulatedV6Bridge control thread ready");
|
||||
byte[] buffer = new byte[1024];
|
||||
DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
|
||||
|
||||
// Now loop forever, waiting to receive packets and printing them.
|
||||
while (!willbeclosed) {
|
||||
rPacket.setLength(buffer.length);
|
||||
datagramSocket.receive(rPacket);
|
||||
|
||||
sPacket.setAddress(rPacket.getAddress());
|
||||
sPacket.setPort(rPacket.getPort());
|
||||
|
||||
int len = rPacket.getLength();
|
||||
|
||||
if (len < 5 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) {
|
||||
logUnknownPacket(buffer, len, "Not an iBox request!");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (len >= sessionRequest.length && buffer[0] == sessionRequest[0]) {
|
||||
sessionRequest[22] = buffer[22];
|
||||
sessionRequest[23] = buffer[23];
|
||||
boolean eq = ByteBuffer.wrap(sessionRequest, 0, sessionRequest.length)
|
||||
.equals(ByteBuffer.wrap(buffer, 0, sessionRequest.length));
|
||||
if (eq) {
|
||||
debugSession("session get message received");
|
||||
sessionResponse[19] = SID1;
|
||||
sessionResponse[20] = SID2;
|
||||
replaceWithMac(sessionResponse, 7);
|
||||
debugSessionSend(sessionResponse, sPacket.getAddress());
|
||||
sendMessage(sPacket, datagramSocket, sessionResponse);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= sessionRequestFindBroadcast.length && buffer[0] == sessionRequestFindBroadcast[0]) {
|
||||
sessionRequestFindBroadcast[6] = buffer[6];
|
||||
sessionRequestFindBroadcast[7] = buffer[7];
|
||||
boolean eq = ByteBuffer.wrap(sessionRequestFindBroadcast, 0, 6)
|
||||
.equals(ByteBuffer.wrap(buffer, 0, 6));
|
||||
if (eq) {
|
||||
debugSession("init session received");
|
||||
cls1 = sessionRequestFindBroadcast[6];
|
||||
cls2 = sessionRequestFindBroadcast[7];
|
||||
replaceWithMac(REESTABLISH_SESSION_RESPONSE, 6);
|
||||
debugSessionSend(REESTABLISH_SESSION_RESPONSE, sPacket.getAddress());
|
||||
sendMessage(sPacket, datagramSocket, REESTABLISH_SESSION_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= searchBroadcast.length && buffer[0] == searchBroadcast[0]) {
|
||||
searchBroadcast[6] = buffer[6];
|
||||
searchBroadcast[7] = buffer[7];
|
||||
boolean eq = ByteBuffer.wrap(searchBroadcast, 0, searchBroadcast.length)
|
||||
.equals(ByteBuffer.wrap(buffer, 0, searchBroadcast.length));
|
||||
if (eq) {
|
||||
debugSession("Search request");
|
||||
cls1 = searchBroadcast[6];
|
||||
cls2 = searchBroadcast[7];
|
||||
replaceWithMac(REESTABLISH_SESSION_RESPONSE, 6);
|
||||
debugSessionSend(REESTABLISH_SESSION_RESPONSE, sPacket.getAddress());
|
||||
sendMessage(sPacket, datagramSocket, REESTABLISH_SESSION_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= ESTABLISH_SESSION_REQUEST.length && buffer[0] == ESTABLISH_SESSION_REQUEST[0]) {
|
||||
ESTABLISH_SESSION_REQUEST[5] = SID1;
|
||||
ESTABLISH_SESSION_REQUEST[6] = SID2;
|
||||
boolean eq = ByteBuffer.wrap(ESTABLISH_SESSION_REQUEST, 0, ESTABLISH_SESSION_REQUEST.length)
|
||||
.equals(ByteBuffer.wrap(buffer, 0, ESTABLISH_SESSION_REQUEST.length));
|
||||
if (eq) {
|
||||
debugSession("Session establish request");
|
||||
replaceWithMac(REESTABLISH_SESSION_RESPONSE, 6);
|
||||
debugSessionSend(REESTABLISH_SESSION_RESPONSE, sPacket.getAddress());
|
||||
sendMessage(sPacket, datagramSocket, REESTABLISH_SESSION_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= KEEP_ALIVE_REQUEST.length && buffer[0] == KEEP_ALIVE_REQUEST[0]) {
|
||||
KEEP_ALIVE_REQUEST[5] = SID1;
|
||||
KEEP_ALIVE_REQUEST[6] = SID2;
|
||||
boolean eq = ByteBuffer.wrap(KEEP_ALIVE_REQUEST, 0, KEEP_ALIVE_REQUEST.length)
|
||||
.equals(ByteBuffer.wrap(buffer, 0, KEEP_ALIVE_REQUEST.length));
|
||||
if (eq) {
|
||||
debugSession("keep alive received");
|
||||
replaceWithMac(KEEP_ALIVE_RESPONSE, 5);
|
||||
debugSessionSend(KEEP_ALIVE_RESPONSE, sPacket.getAddress());
|
||||
sendMessage(sPacket, datagramSocket, KEEP_ALIVE_RESPONSE);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= REGISTRATION_REQUEST.length && buffer[0] == REGISTRATION_REQUEST[0]) {
|
||||
byte seq = buffer[8];
|
||||
if (buffer[5] != SID1 || buffer[6] != SID2) {
|
||||
logUnknownPacket(buffer, len,
|
||||
"No valid ssid. Current ssid is " + String.format("%02X %02X", SID1, SID2));
|
||||
continue;
|
||||
}
|
||||
if (buffer[11] != PW1 || buffer[12] != PW2) {
|
||||
logUnknownPacket(buffer, len,
|
||||
"No valid password. Current password is " + String.format("%02X %02X", PW1, PW2));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer[4] == 0x11) {
|
||||
if (buffer[10] == 0x33) {
|
||||
replaceWithMac(REGISTRATION_REQUEST_RESPONSE, 5);
|
||||
sendMessage(sPacket, datagramSocket, REGISTRATION_REQUEST_RESPONSE);
|
||||
} else if (buffer[10] == 0x31) {
|
||||
// 80 00 00 00 11 {WifiBridgeSessionID1} {WifiBridgeSessionID2} 00 {SequenceNumber} 00 0x31
|
||||
// {PasswordByte1 default 00} {PasswordByte2 default 00} {remoteStyle 08 for RGBW/WW/CW or
|
||||
// 00 for bridge lamp} {LightCommandByte1} {LightCommandByte2} 0 0 0 {Zone1-4 0=All} 0
|
||||
// {Checksum}
|
||||
|
||||
byte chksum = (byte) (buffer[10 + 0] + buffer[10 + 1] + buffer[10 + 2] + buffer[10 + 3]
|
||||
+ buffer[10 + 4] + buffer[10 + 5] + buffer[10 + 6] + buffer[10 + 7] + buffer[10 + 8]
|
||||
+ buffer[19]);
|
||||
|
||||
if (chksum != buffer[21]) {
|
||||
logger.error("Checksum wrong:{} {}", chksum, buffer[21]);
|
||||
continue;
|
||||
}
|
||||
|
||||
StringBuilder debugStr = new StringBuilder();
|
||||
if (buffer[13] == 0x08) {
|
||||
debugStr.append("RGBWW ");
|
||||
} else if (buffer[13] == 0x07) {
|
||||
debugStr.append("RGBW ");
|
||||
} else {
|
||||
debugStr.append("iBox ");
|
||||
}
|
||||
|
||||
debugStr.append("Zone " + String.valueOf(buffer[19]) + " ");
|
||||
|
||||
for (int i = 13; i < 19; ++i) {
|
||||
debugStr.append(String.format("%02X ", buffer[i]));
|
||||
}
|
||||
logger.debug("{}", debugStr);
|
||||
}
|
||||
}
|
||||
|
||||
byte response[] = { (byte) 0x88, 0, 0, 0, (byte) 0x03, 0, seq, 0 };
|
||||
sendMessage(sPacket, datagramSocket, response);
|
||||
continue;
|
||||
}
|
||||
|
||||
logUnknownPacket(buffer, len, "Not recognised command");
|
||||
}
|
||||
} catch (
|
||||
|
||||
IOException e) {
|
||||
if (willbeclosed) {
|
||||
return;
|
||||
}
|
||||
logger.error("{}", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected void logUnknownPacket(byte[] data, int len, String reason) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int i = 0; i < len; ++i) {
|
||||
s.append(String.format("%02X ", data[i]));
|
||||
}
|
||||
logger.error("{}: {}", reason, s);
|
||||
}
|
||||
|
||||
protected void sendMessage(DatagramPacket packet, DatagramSocket datagramSocket, byte buffer[]) {
|
||||
packet.setData(buffer);
|
||||
try {
|
||||
datagramSocket.send(packet);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to send Message to '{}', Error message: {}",
|
||||
new Object[] { packet.getAddress().getHostAddress(), e.getMessage() });
|
||||
}
|
||||
}
|
||||
|
||||
private void debugSessionSend(byte buffer[], InetAddress address) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
for (int i = 0; i < buffer.length; ++i) {
|
||||
s.append(String.format("%02X ", buffer[i]));
|
||||
}
|
||||
// logger.debug("Sent packet '{}' to ({})", new Object[] { s.toString(), address.getHostAddress() });
|
||||
}
|
||||
|
||||
private void debugSession(String msg) {
|
||||
// logger.debug(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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.milight.internal.test;
|
||||
|
||||
import org.openhab.binding.milight.internal.MilightBindingConstants;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A simple discovery service that will make the emulated V6 bridge visible to the Inbox of OH.
|
||||
* Enable this in OSGI-INF/TestDiscovery.xml with enabled="true".
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(service = DiscoveryService.class, immediate = true, enabled = false)
|
||||
public class TestDiscovery extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(TestDiscovery.class);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private EmulatedV6Bridge server;
|
||||
|
||||
public TestDiscovery() {
|
||||
super(MilightBindingConstants.BRIDGE_THING_TYPES_UIDS, 2, true);
|
||||
try {
|
||||
server = new EmulatedV6Bridge();
|
||||
} catch (Exception e) {
|
||||
logger.warn("An error occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="milight" 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>Milight Binding</name>
|
||||
<description>A binding for Milight/Easybulb/compatible white, color and color+white bulbs.</description>
|
||||
<author>David Gräff</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="milight"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-type id="lednightmode">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Nightmode</label>
|
||||
<description>Switch to night mode, a very dimmed brightness mode</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="ON">Nightmode</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledwhitemode">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Whitemode</label>
|
||||
<description>Switch to white mode, which basically sets the saturation to 0 (turns off the color leds)</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="ON">Whitemode</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledlink" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Link Bulb</label>
|
||||
<description>Sync bulb to this zone within 3 seconds of light bulb socket power on</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledunlink" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Unlink Bulb</label>
|
||||
<description>Clear bulb from this zone within 3 seconds of light bulb socket power on</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledcolor">
|
||||
<item-type>Color</item-type>
|
||||
<label>Color</label>
|
||||
<description>Color of the LED. Bind to a Dimmer to just set the brightness, bind to a Color chooser for the full
|
||||
control and bind to a Switch for turning the led on or off.
|
||||
</description>
|
||||
<category>ColorLight</category>
|
||||
<tags>
|
||||
<tag>ColorLighting</tag>
|
||||
<tag>Lighting</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledbrightness">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Brightness</label>
|
||||
<description>The brightness can be set in 16 steps for RGBW/White leds and in 64 steps for RGBWW leds</description>
|
||||
<category>Light</category>
|
||||
<tags>
|
||||
<tag>Lighting</tag>
|
||||
</tags>
|
||||
<state min="0" max="100" step="1" pattern="%d"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledsaturation" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Saturation</label>
|
||||
<description>The saturation can be set in 64 steps for RGBWW leds</description>
|
||||
<state min="0" max="100" step="1" pattern="%d"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="ledtemperature">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Color Temperature</label>
|
||||
<description>White leds and RGBWW allow to change between a cold and a warm color temperature. White support 16, RGBWW
|
||||
support 64 steps
|
||||
</description>
|
||||
<category>DimmableLight</category>
|
||||
<state min="0" max="100" step="1" pattern="%d"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="animation_speed_relative">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Animation Speed</label>
|
||||
<description>The speed of some animations can be increased or decreased</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="animation_mode_relative">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Animation Mode</label>
|
||||
<description>Switch to the next/previous animation mode of your RGBW or white LED. Bind this to a Next/Previous
|
||||
channel type.
|
||||
</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="animation_mode">
|
||||
<item-type>Number</item-type>
|
||||
<label>Animation Mode</label>
|
||||
<description>Animation mode of your LED. RGBWW leds support 9 animation modes.</description>
|
||||
<category>Light</category>
|
||||
<state>
|
||||
<options>
|
||||
<option value="1">Animation 1</option>
|
||||
<option value="2">Animation 2</option>
|
||||
<option value="3">Animation 3</option>
|
||||
<option value="4">Animation 4</option>
|
||||
<option value="5">Animation 5</option>
|
||||
<option value="6">Animation 6</option>
|
||||
<option value="7">Animation 7</option>
|
||||
<option value="8">Animation 8</option>
|
||||
<option value="9">Animation 9</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="milight"
|
||||
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="rgbv2Led">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridgeV3"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Color Bulb (old)</label>
|
||||
<description>The oldest produced color bulb. Without a white channel. No saturation support.</description>
|
||||
<category>Lightbulb</category>
|
||||
|
||||
<channels>
|
||||
<channel id="ledbrightness" typeId="ledbrightness"/>
|
||||
<channel id="ledcolor" typeId="ledcolor"/>
|
||||
<channel id="animation_mode_relative" typeId="animation_mode_relative"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zone" type="integer" required="true">
|
||||
<label>Zone</label>
|
||||
<description>A milight bulb can be assigned to zone 0-4. zone 0 controls all bulbs of that type.
|
||||
</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="milight"
|
||||
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="bridgeV3">
|
||||
<label>Milight Bridge (first Edition)</label>
|
||||
<description>A Milight/Easybulb bridge. This bridge cannot handle newer light bulbs (2016+) of the aforementioned
|
||||
manufacturers.</description>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text">
|
||||
<label>IP or Host Name</label>
|
||||
<description>You either need an IP/Hostname or the Bridge ID.</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="repeat" type="integer" required="false" min="0" max="5">
|
||||
<label>Repeat Commands</label>
|
||||
<description>Usually the bridge receives all commands albeit UDP is used. But the actual bulbs might be slightly out
|
||||
of bridge radio range and it sometimes helps to send commands multiple times.
|
||||
</description>
|
||||
<default>1</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="delayTime" type="integer" required="false" min="0" max="400">
|
||||
<label>Wait Between Commands (ms)</label>
|
||||
<description>Time to wait before sending another command to the bridge. It is safe to have a wait time of 1/10s but
|
||||
usually sufficient to just wait 50ms. If the value is too high, commands queue up.
|
||||
</description>
|
||||
<default>100</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="false">
|
||||
<label>Custom Port</label>
|
||||
<description>You can set a custom port that will take precedence over the default port which is selected depending
|
||||
on the bridge version: Version 6 uses 5987, Version 3/4/5 uses 8899. Version 2 uses 50000.
|
||||
</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="bridgeid" type="text">
|
||||
<label>Bridge ID</label>
|
||||
<description>The mac address of the bridge in upper case letters without delimiter.
|
||||
Use this parameter and leave the
|
||||
IP/Hostname empty for DHCP environments where IPs may often change over time.
|
||||
|
||||
The Bridge ID is also used to check if
|
||||
a given IP corresponds to the right device.
|
||||
The bridge is set offline if the device does not respond with the
|
||||
correct Bridge ID and a re-detection is started.
|
||||
</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="refreshTime" type="integer" min="5" max="300">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval in seconds to check for device presence. The Bridge ID is used to check if the IP is still the
|
||||
right one.
|
||||
</description>
|
||||
<default>10</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="rgbLed">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridgeV3"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Color Bulb (first Edition)</label>
|
||||
<description>RGB bulb with white channel. No saturation control. If the set saturation is below a threshold of 50%,
|
||||
the bulb turns into white mode.</description>
|
||||
<category>Lightbulb</category>
|
||||
|
||||
<channels>
|
||||
<channel id="lednightmode" typeId="lednightmode"/>
|
||||
<channel id="ledwhitemode" typeId="ledwhitemode"/>
|
||||
<channel id="ledbrightness" typeId="ledbrightness"/>
|
||||
<channel id="ledcolor" typeId="ledcolor"/>
|
||||
<channel id="animation_speed_relative" typeId="animation_speed_relative"/>
|
||||
<channel id="animation_mode_relative" typeId="animation_mode_relative"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zone" type="integer" required="true">
|
||||
<label>Zone</label>
|
||||
<description>A milight bulb can be assigned to zone 0-4. zone 0 controls all bulbs of that type.
|
||||
</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="whiteLed">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridgeV3"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Cold/warm White Bulb (first Edition)</label>
|
||||
<description>White bulb for the older bridge (up to 2016)</description>
|
||||
<category>Lightbulb</category>
|
||||
|
||||
<channels>
|
||||
<channel id="lednightmode" typeId="lednightmode"/>
|
||||
<channel id="ledbrightness" typeId="ledbrightness"/>
|
||||
<channel id="ledtemperature" typeId="ledtemperature"/>
|
||||
<channel id="animation_mode_relative" typeId="animation_mode_relative"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zone" type="integer" required="true">
|
||||
<label>Zone</label>
|
||||
<description>A milight bulb can be assigned to zone 0-4. zone 0 controls all bulbs of that type.
|
||||
</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="milight"
|
||||
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="bridgeV6">
|
||||
<label>Milight Bridge (iBox)</label>
|
||||
<description>A bridge for all newer light bulbs (2016+) of the Milight/Easybulb system.</description>
|
||||
|
||||
<properties>
|
||||
<property name="sessionid">N/A</property>
|
||||
<property name="sessionid_last_refresh">N/A</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>IP or Host Name</label>
|
||||
<description>Will be resolved by discovery if auto configured
|
||||
</description>
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="passwordByte1" type="integer" required="true" min="0" max="255">
|
||||
<label>Password Byte 1</label>
|
||||
<description>Bridge V6 allows to set two password bytes. A value from 0-255 is allowed.
|
||||
</description>
|
||||
<context>password</context>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="passwordByte2" type="integer" required="true" min="0" max="255">
|
||||
<label>Password Byte 2</label>
|
||||
<description>Bridge V6 allows to set two password bytes. A value from 0-255 is allowed.
|
||||
</description>
|
||||
<context>password</context>
|
||||
<default>0</default>
|
||||
</parameter>
|
||||
<parameter name="repeat" type="integer" required="false" min="0" max="5">
|
||||
<label>Repeat Commands</label>
|
||||
<description>Usually the bridge receives all commands albeit UDP is used. But the actual bulbs might be slightly out
|
||||
of bridge radio range and it sometimes helps to send commands multiple times.
|
||||
</description>
|
||||
<default>1</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="delayTime" type="integer" required="false" min="0" max="400">
|
||||
<label>Wait Between Commands (ms)</label>
|
||||
<description>Time to wait before sending another command to the bridge. It is safe to have a wait time of 1/10s but
|
||||
usually sufficient to just wait 50ms. If the value is too high, commands queue up.
|
||||
</description>
|
||||
<default>100</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer" required="false">
|
||||
<label>Custom Port</label>
|
||||
<description>You can set a custom port that will take precedence over the default port which is selected depending
|
||||
on the bridge version: Version 6 uses 5987, Version 3/4/5 uses 8899. Version 2 uses 50000.
|
||||
</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="bridgeid" type="text" required="true">
|
||||
<label>Bridge ID</label>
|
||||
<description>The mac address of the bridge in upper case letters without delimiter.
|
||||
This is used to check if the
|
||||
given IP corresponds to the right device. The bridge is set offline if the device
|
||||
does not respond with the correct
|
||||
Bride ID and a re-detection is started. Useful for DHCP environments where
|
||||
IPs may change over time, after power
|
||||
outage etc. Will be resolved by discovery if auto configured.
|
||||
</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="refreshTime" type="integer" min="100" max="10000" required="true">
|
||||
<label>Keep Alive Interval</label>
|
||||
<description>Interval in milliseconds to send a keep alive ping. If the value is too high, a session may expire and
|
||||
the bridge and all devices could go offline for a few seconds.
|
||||
</description>
|
||||
<default>5000</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="rgbiboxLed">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridgeV6"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Integrated Bulb (iBox)</label>
|
||||
<description>Integrated bulb of the ibox bridge with no dedicated white channel and therefore no saturation control
|
||||
</description>
|
||||
<category>Lightbulb</category>
|
||||
|
||||
<channels>
|
||||
<channel id="ledwhitemode" typeId="ledwhitemode"/>
|
||||
<channel id="ledbrightness" typeId="ledbrightness"/>
|
||||
<channel id="ledcolor" typeId="ledcolor"/>
|
||||
<channel id="animation_speed_relative" typeId="animation_speed_relative"/>
|
||||
<channel id="animation_mode" typeId="animation_mode"/>
|
||||
<channel id="animation_mode_relative" typeId="animation_mode_relative"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zone" type="integer" required="true">
|
||||
<label>Zone</label>
|
||||
<description>A milight bulb can be assigned to zone 0-4. zone 0 controls all bulbs of that type.
|
||||
</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="rgbwLed">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridgeV6"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Color Bulb with Cold White (iBox)</label>
|
||||
<description>Color bulb with white channel for the new Milight/Easybulb system.</description>
|
||||
<category>Lightbulb</category>
|
||||
|
||||
<channels>
|
||||
<channel id="ledlink" typeId="ledlink"/>
|
||||
<channel id="ledunlink" typeId="ledunlink"/>
|
||||
<channel id="lednightmode" typeId="lednightmode"/>
|
||||
<channel id="ledwhitemode" typeId="ledwhitemode"/>
|
||||
<channel id="ledbrightness" typeId="ledbrightness"/>
|
||||
<channel id="ledcolor" typeId="ledcolor"/>
|
||||
<channel id="animation_speed_relative" typeId="animation_speed_relative"/>
|
||||
<channel id="animation_mode" typeId="animation_mode"/>
|
||||
<channel id="animation_mode_relative" typeId="animation_mode_relative"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zone" type="integer" required="true">
|
||||
<label>Zone</label>
|
||||
<description>A milight bulb can be assigned to zone 0-4. zone 0 controls all bulbs of that type.
|
||||
</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="rgbwwLed">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridgeV6"/>
|
||||
</supported-bridge-type-refs>
|
||||
<label>Color Bulb with Cold/warm White (iBox)</label>
|
||||
<description>Color bulb with warm and cold white support for the new Milight/Easybulb system.</description>
|
||||
<category>Lightbulb</category>
|
||||
|
||||
<channels>
|
||||
<channel id="ledlink" typeId="ledlink"/>
|
||||
<channel id="ledunlink" typeId="ledunlink"/>
|
||||
<channel id="lednightmode" typeId="lednightmode"/>
|
||||
<channel id="ledwhitemode" typeId="ledwhitemode"/>
|
||||
<channel id="ledtemperature" typeId="ledtemperature"/>
|
||||
<channel id="ledbrightness" typeId="ledbrightness"/>
|
||||
<channel id="ledsaturation" typeId="ledsaturation"/>
|
||||
<channel id="ledcolor" typeId="ledcolor"/>
|
||||
<channel id="animation_speed_relative" typeId="animation_speed_relative"/>
|
||||
<channel id="animation_mode" typeId="animation_mode"/>
|
||||
<channel id="animation_mode_relative" typeId="animation_mode_relative"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="zone" type="integer" required="true">
|
||||
<label>Zone</label>
|
||||
<description>A milight bulb can be assigned to zone 0-4. zone 0 controls all bulbs of that type.
|
||||
</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user