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; }