2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.milight.internal.handler;
15 import java.io.IOException;
16 import java.net.DatagramPacket;
17 import java.net.DatagramSocket;
18 import java.net.InetAddress;
19 import java.net.InetSocketAddress;
20 import java.net.SocketException;
21 import java.net.SocketTimeoutException;
22 import java.net.UnknownHostException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.milight.internal.MilightBindingConstants;
29 import org.openhab.core.config.core.Configuration;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
36 * The {@link BridgeV3Handler} is responsible for handling commands, which are
37 * sent to one of the channels.
39 * @author David Graeff - Initial contribution
42 public class BridgeV3Handler extends AbstractBridgeHandler {
43 protected final DatagramPacket discoverPacketV3;
44 protected final byte[] buffer = new byte[1024];
45 protected final DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
46 private @Nullable ScheduledFuture<?> running;
48 public BridgeV3Handler(Bridge bridge, int bridgeOffset) {
49 super(bridge, bridgeOffset);
50 discoverPacketV3 = new DatagramPacket(MilightBindingConstants.DISCOVER_MSG_V3,
51 MilightBindingConstants.DISCOVER_MSG_V3.length);
55 public void thingUpdated(Thing thing) {
56 super.thingUpdated(thing);
60 * Creates a discovery object and the send queue. The initial IP address may be null
61 * or is not matching with the real IP address of the bridge. The discovery class will send
62 * a broadcast packet to find the bridge with the respective bridge ID. The response in bridgeDetected()
63 * may lead to a recreation of the send queue object.
65 * The keep alive timer that is also setup here, will send keep alive packets periodically.
66 * If the bridge doesn't respond anymore (e.g. DHCP IP change), the initial session handshake
67 * starts all over again.
70 protected void startConnectAndKeepAlive() {
71 if (address == null) {
72 if (!config.bridgeid.matches("^([0-9A-Fa-f]{12})$")) {
73 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridgeID invalid!");
77 address = InetAddress.getByAddress(new byte[] { (byte) 255, (byte) 255, (byte) 255, (byte) 255 });
78 } catch (UnknownHostException neverHappens) {
82 if (config.port == 0) {
83 config.port = MilightBindingConstants.PORT_VER3;
87 socket = new DatagramSocket(null);
88 socket.setReuseAddress(true);
89 socket.setBroadcast(true);
91 } catch (SocketException e) {
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
95 running = scheduler.scheduleWithFixedDelay(this::receive, 0, config.refreshTime, TimeUnit.SECONDS);
98 protected void stopKeepAlive() {
99 if (running != null) {
100 running.cancel(false);
103 if (socket != null) {
109 public void dispose() {
113 private void receive() {
115 discoverPacketV3.setAddress(address);
116 discoverPacketV3.setPort(MilightBindingConstants.PORT_DISCOVER);
118 final int attempts = 5;
119 int timeoutsCounter = 0;
120 for (timeoutsCounter = 1; timeoutsCounter <= attempts; ++timeoutsCounter) {
122 packet.setLength(buffer.length);
123 socket.setSoTimeout(500 * timeoutsCounter);
124 socket.send(discoverPacketV3);
125 socket.receive(packet);
126 } catch (SocketTimeoutException e) {
129 // We expect packets with a format like this: 10.1.1.27,ACCF23F57AD4,HF-LPB100
130 final String received = new String(packet.getData());
131 final String[] msg = received.split(",");
133 if (msg.length != 2 && msg.length != 3) {
134 // That data packet does not belong to a Milight bridge. Just ignore it.
138 // First argument is the IP
140 InetAddress.getByName(msg[0]);
141 } catch (UnknownHostException ignored) {
142 // That data packet does not belong to a Milight bridge, we expect an IP address as first
143 // argument. Just ignore it.
147 // Second argument is the MAC address
148 if (msg[1].length() != 12) {
149 // That data packet does not belong to a Milight bridge, we expect a MAC address as second
155 final InetAddress addressOfBridge = ((InetSocketAddress) packet.getSocketAddress()).getAddress();
156 final String bridgeID = msg[1];
158 if (!config.bridgeid.isEmpty() && !bridgeID.equals(config.bridgeid)) {
159 // We found a bridge, but it is not the one that is handled by this handler
160 if (!config.host.isEmpty()) { // The user has set a host address -> but wrong bridge found!
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
163 "Wrong bridge found on host address. Change bridgeid or host configuration.");
169 // IP address has changed, reestablish communication
170 if (!addressOfBridge.equals(this.address)) {
171 this.address = addressOfBridge;
172 Configuration c = editConfiguration();
173 c.put(BridgeHandlerConfig.CONFIG_HOST_NAME, addressOfBridge.getHostAddress());
174 preventReinit = true;
175 updateConfiguration(c);
176 preventReinit = false;
177 } else if (config.bridgeid.isEmpty()) { // bridge id was not set and is now known. Store it.
178 config.bridgeid = bridgeID;
179 Configuration c = editConfiguration();
180 c.put(BridgeHandlerConfig.CONFIG_BRIDGE_ID, bridgeID);
181 preventReinit = true;
182 updateConfiguration(c);
183 preventReinit = false;
186 updateStatus(ThingStatus.ONLINE);
189 if (timeoutsCounter > attempts) {
190 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Bridge did not respond!");
192 } catch (IOException e) {
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());