diff --git a/src/main/feature/feature.xml b/src/main/feature/feature.xml
index d90d48b..b246141 100644
--- a/src/main/feature/feature.xml
+++ b/src/main/feature/feature.xml
@@ -7,6 +7,6 @@
openhab-transport-mdns
mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}
- mvn:org.openhab.addons.bundles/no.seime.openhab.binding.esphome/${project.version}
+ mvn:org.openhab.addons.bundles/no.seime.openhab.binding.esphome/${project.version}
diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/bluetooth/ESPHomeBluetoothProxyHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/bluetooth/ESPHomeBluetoothProxyHandler.java
index 19772d8..911b97a 100644
--- a/src/main/java/no/seime/openhab/binding/esphome/internal/bluetooth/ESPHomeBluetoothProxyHandler.java
+++ b/src/main/java/no/seime/openhab/binding/esphome/internal/bluetooth/ESPHomeBluetoothProxyHandler.java
@@ -1,11 +1,12 @@
package no.seime.openhab.binding.esphome.internal.bluetooth;
-import java.util.ArrayList;
-import java.util.HexFormat;
-import java.util.List;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import io.esphome.api.BluetoothLEAdvertisementResponse;
+import no.seime.openhab.binding.esphome.internal.BindingConstants;
+import no.seime.openhab.binding.esphome.internal.handler.ESPHomeHandler;
+import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.AbstractBluetoothBridgeHandler;
@@ -15,9 +16,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import io.esphome.api.BluetoothLEAdvertisementResponse;
-import no.seime.openhab.binding.esphome.internal.BindingConstants;
-import no.seime.openhab.binding.esphome.internal.handler.ESPHomeHandler;
+import java.util.ArrayList;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
@NonNullByDefault
public class ESPHomeBluetoothProxyHandler extends AbstractBluetoothBridgeHandler {
@@ -31,6 +36,8 @@ public class ESPHomeBluetoothProxyHandler extends AbstractBluetoothBridgeHandler
private List espHomeHandlers = new ArrayList<>();
+ private final LoadingCache> cache;
+
/**
* Creates a new instance of this class for the {@link Thing}.
*
@@ -40,10 +47,21 @@ public class ESPHomeBluetoothProxyHandler extends AbstractBluetoothBridgeHandler
public ESPHomeBluetoothProxyHandler(Bridge thing, ThingRegistry thingRegistry) {
super(thing);
this.thingRegistry = thingRegistry;
+ CacheLoader> loader;
+ loader = new CacheLoader<>() {
+
+ @Override
+ public Optional load(Long key) {
+ return Optional.empty();
+ }
+ };
+
+ cache = CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.SECONDS).maximumSize(1000).build(loader);
}
@Override
public void initialize() {
+
super.initialize();
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Looking for BLE enabled ESPHome devices");
@@ -60,7 +78,6 @@ public void dispose() {
private synchronized void updateESPHomeDeviceList() {
- logger.debug("Updating list of ESPHome devices");
// Get all ESPHome devices
// For each device, check if it has BLE enabled, is enabled and ONLINE
// If so, enable registration of BLE advertisements
@@ -104,6 +121,8 @@ private synchronized void updateESPHomeDeviceList() {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, String
.format("Found %d ESPHome devices configured for Bluetooth proxy support", espHomeHandlers.size()));
}
+ logger.debug("List of {} ESPHome devices: {}", espHomeHandlers.size(),
+ espHomeHandlers.stream().map(e -> e.getThing().getUID()).toList());
}
@Override
@@ -131,25 +150,35 @@ private BluetoothAddress createAddress(long address) {
return new BluetoothAddress(addressBuilder.toString().toUpperCase());
}
- public void handleAdvertisement(BluetoothLEAdvertisementResponse rsp) {
+ public void handleAdvertisement(@NonNull BluetoothLEAdvertisementResponse rsp, ESPHomeHandler handler) {
+
+ try {
+ Optional cachedAdvertisement = cache.get(rsp.getAddress());
+ if (cachedAdvertisement.isPresent() && equalsExceptRssi(rsp, cachedAdvertisement.get())) {
+ logger.debug("Received duplicate BLE advertisement from device {} via {}", rsp.getAddress(),
+ handler.getThing().getUID());
+ return;
+ } else {
+ cache.put(rsp.getAddress(), Optional.of(rsp));
+ }
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
try {
BluetoothAddress address = createAddress(rsp.getAddress());
ESPHomeBluetoothDevice device = getDevice(address);
- logger.debug("Received BLE advertisement from device {}", address);
+ logger.debug("Received BLE advertisement from device {} via {}", address, handler.getThing().getUID());
device.setName(rsp.getName());
device.setRssi(rsp.getRssi());
rsp.getManufacturerDataList().stream().findFirst().ifPresent(manufacturerData -> {
String uuid = manufacturerData.getUuid();
- byte[] bytes = HexFormat.of().parseHex(uuid.substring(2));
- int manufacturerId = (bytes[0] & 0xFF) << 8 | (bytes[1] & 0xFF);
+ int manufacturerId = parseManufacturerIdToInt(uuid);
device.setManufacturerId(manufacturerId);
- logger.debug("Manufacturer data UUID: {}", uuid);
-
});
deviceDiscovered(device);
@@ -161,9 +190,23 @@ public void handleAdvertisement(BluetoothLEAdvertisementResponse rsp) {
}
}
+ private boolean equalsExceptRssi(BluetoothLEAdvertisementResponse rsp1, BluetoothLEAdvertisementResponse rsp2) {
+ return rsp1.getAddress() == rsp2.getAddress() && rsp1.getName().equals(rsp2.getName())
+ && rsp1.getManufacturerDataList().equals(rsp2.getManufacturerDataList())
+ && rsp1.getServiceDataList().equals(rsp2.getServiceDataList())
+ && rsp1.getServiceUuidsList().equals(rsp2.getServiceUuidsList())
+ && rsp1.getAddressType() == rsp2.getAddressType();
+ }
+
+ private int parseManufacturerIdToInt(String uuid) {
+ byte[] bytes = HexFormat.of().parseHex(uuid.substring(2));
+ int manufacturerId = (bytes[0] & 0xFF) << 8 | (bytes[1] & 0xFF);
+ logger.debug("Manufacturer UUID: {} -> {}", uuid, manufacturerId);
+ return manufacturerId;
+ }
+
@Override
public @Nullable BluetoothAddress getAddress() {
- // Return adapter/ESPHome address
return null;
}
}
diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/comm/ESPHomeConnection.java b/src/main/java/no/seime/openhab/binding/esphome/internal/comm/ESPHomeConnection.java
index b644541..7fd387f 100644
--- a/src/main/java/no/seime/openhab/binding/esphome/internal/comm/ESPHomeConnection.java
+++ b/src/main/java/no/seime/openhab/binding/esphome/internal/comm/ESPHomeConnection.java
@@ -38,14 +38,18 @@ public ESPHomeConnection(ConnectionSelector connectionSelector, AbstractFrameHel
}
public synchronized void send(ByteBuffer buffer) throws ProtocolAPIError {
- try {
- while (buffer.hasRemaining()) {
- logger.trace("[{}] Writing data", logPrefix);
- socketChannel.write(buffer);
- }
+ if (socketChannel != null) {
+ try {
+ while (buffer.hasRemaining()) {
+ logger.trace("[{}] Writing data", logPrefix);
+ socketChannel.write(buffer);
+ }
- } catch (IOException e) {
- throw new ProtocolAPIError(String.format("[%s] Error sending message: %s ", logPrefix, e));
+ } catch (IOException e) {
+ throw new ProtocolAPIError(String.format("[%s] Error sending message: %s ", logPrefix, e));
+ }
+ } else {
+ logger.warn("[{}] Attempted to send data on a closed connection", logPrefix);
}
}
diff --git a/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java b/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java
index 7febb0e..5bdf637 100644
--- a/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java
+++ b/src/main/java/no/seime/openhab/binding/esphome/internal/handler/ESPHomeHandler.java
@@ -12,13 +12,17 @@
*/
package no.seime.openhab.binding.esphome.internal.handler;
-import java.math.BigDecimal;
-import java.net.InetSocketAddress;
-import java.time.Instant;
-import java.util.*;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
+import com.google.protobuf.GeneratedMessageV3;
+import io.esphome.api.*;
+import no.seime.openhab.binding.esphome.internal.BindingConstants;
+import no.seime.openhab.binding.esphome.internal.CommunicationListener;
+import no.seime.openhab.binding.esphome.internal.ESPHomeConfiguration;
+import no.seime.openhab.binding.esphome.internal.LogLevel;
+import no.seime.openhab.binding.esphome.internal.bluetooth.ESPHomeBluetoothProxyHandler;
+import no.seime.openhab.binding.esphome.internal.comm.*;
+import no.seime.openhab.binding.esphome.internal.message.*;
+import no.seime.openhab.binding.esphome.internal.message.statesubscription.ESPHomeEventSubscriber;
+import no.seime.openhab.binding.esphome.internal.message.statesubscription.EventSubscription;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -32,18 +36,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.protobuf.GeneratedMessageV3;
-
-import io.esphome.api.*;
-import no.seime.openhab.binding.esphome.internal.BindingConstants;
-import no.seime.openhab.binding.esphome.internal.CommunicationListener;
-import no.seime.openhab.binding.esphome.internal.ESPHomeConfiguration;
-import no.seime.openhab.binding.esphome.internal.LogLevel;
-import no.seime.openhab.binding.esphome.internal.bluetooth.ESPHomeBluetoothProxyHandler;
-import no.seime.openhab.binding.esphome.internal.comm.*;
-import no.seime.openhab.binding.esphome.internal.message.*;
-import no.seime.openhab.binding.esphome.internal.message.statesubscription.ESPHomeEventSubscriber;
-import no.seime.openhab.binding.esphome.internal.message.statesubscription.EventSubscription;
+import java.math.BigDecimal;
+import java.net.InetSocketAddress;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
/**
* The {@link ESPHomeHandler} is responsible for handling commands, which are
@@ -211,6 +209,8 @@ public void dispose() {
frameHelper.close();
}
}
+ connectionState = ConnectionState.UNINITIALIZED;
+
super.dispose();
}
@@ -329,6 +329,10 @@ private void remoteDisconnect() {
}
private void handleConnected(GeneratedMessageV3 message) throws ProtocolAPIError {
+ if (disposed) {
+ return;
+ }
+
logger.debug("[{}] Received message {}", logPrefix, message);
if (message instanceof DeviceInfoResponse rsp) {
Map props = new HashMap<>();
@@ -366,7 +370,7 @@ private void handleConnected(GeneratedMessageV3 message) throws ProtocolAPIError
frameHelper.send(getTimeResponse);
} else if (message instanceof BluetoothLEAdvertisementResponse rsp) {
if (espHomeBluetoothProxyHandler != null) {
- espHomeBluetoothProxyHandler.handleAdvertisement(rsp);
+ espHomeBluetoothProxyHandler.handleAdvertisement(rsp, this);
}
} else {
// Regular messages handled by message handlers
@@ -515,7 +519,7 @@ public void listenForBLEAdvertisements(ESPHomeBluetoothProxyHandler espHomeBluet
try {
frameHelper.send(SubscribeBluetoothLEAdvertisementsRequest.getDefaultInstance());
bluetoothProxyStarted = true;
- } catch (ProtocolAPIError e) {
+ } catch (Exception e) {
logger.error("[{}] Error starting BLE proxy", logPrefix, e);
}
} else {
@@ -524,13 +528,16 @@ public void listenForBLEAdvertisements(ESPHomeBluetoothProxyHandler espHomeBluet
}
public void stopListeningForBLEAdvertisements() {
- try {
- frameHelper.send(UnsubscribeBluetoothLEAdvertisementsRequest.getDefaultInstance());
- bluetoothProxyStarted = false;
- } catch (ProtocolAPIError e) {
- logger.error("[{}] Error stopping BLE proxy", logPrefix, e);
+
+ if (connectionState == ConnectionState.CONNECTED) {
+ try {
+ frameHelper.send(UnsubscribeBluetoothLEAdvertisementsRequest.getDefaultInstance());
+ } catch (Exception e) {
+ logger.warn("[{}] Error stopping BLE proxy", logPrefix, e);
+ }
}
+ bluetoothProxyStarted = false;
espHomeBluetoothProxyHandler = null;
}