Skip to content

Commit

Permalink
Deduplicate identical advertising packets within 5 seconds window
Browse files Browse the repository at this point in the history
  • Loading branch information
seime committed Jun 24, 2024
1 parent 339dd2b commit f33f7db
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 51 deletions.
2 changes: 1 addition & 1 deletion src/main/feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
<feature>openhab-transport-mdns</feature>

<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/no.seime.openhab.binding.esphome/${project.version}</bundle>
<bundle start-level="81">mvn:org.openhab.addons.bundles/no.seime.openhab.binding.esphome/${project.version}</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<ESPHomeBluetoothDevice> {
Expand All @@ -31,6 +36,8 @@ public class ESPHomeBluetoothProxyHandler extends AbstractBluetoothBridgeHandler

private List<ESPHomeHandler> espHomeHandlers = new ArrayList<>();

private final LoadingCache<Long, Optional<BluetoothLEAdvertisementResponse>> cache;

/**
* Creates a new instance of this class for the {@link Thing}.
*
Expand All @@ -40,10 +47,21 @@ public class ESPHomeBluetoothProxyHandler extends AbstractBluetoothBridgeHandler
public ESPHomeBluetoothProxyHandler(Bridge thing, ThingRegistry thingRegistry) {
super(thing);
this.thingRegistry = thingRegistry;
CacheLoader<Long, Optional<BluetoothLEAdvertisementResponse>> loader;
loader = new CacheLoader<>() {

@Override
public Optional<BluetoothLEAdvertisementResponse> 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");

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<BluetoothLEAdvertisementResponse> 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);
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -211,6 +209,8 @@ public void dispose() {
frameHelper.close();
}
}
connectionState = ConnectionState.UNINITIALIZED;

super.dispose();
}

Expand Down Expand Up @@ -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<String, String> props = new HashMap<>();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}

Expand Down

0 comments on commit f33f7db

Please sign in to comment.