Bluetooth
Since version 0.12.0
The Bluetooth Low Energy (BLE) API enables scripts to scan for and interact with Bluetooth devices, including beacons, sensors, and other BLE peripherals.
Overview
The BLE global object provides:
- Scanner - Discover and monitor Bluetooth devices
- GAP - Parse advertisement data using Generic Access Profile utilities
- AdvBuilder - Advertising support
Bluetooth functionality requires specific hardware support. Not all Shelly devices have Bluetooth capabilities. Check your device specifications.
Since firmware 1.5.0, the Enhanced Scan Manager handles all Bluetooth scanning. Multiple scripts can request scans simultaneously, and the manager will merge scan parameters using the most aggressive settings:
- Shortest
duration_msthat satisfies all requests activescan if any request requires it- Shortest
interval_msamong all requests - Longest
window_msamong all requests (max 100ms) - Lowest
rssi_thramong all requests - Each script's filters are applied independently
This means your actual scan parameters may differ from what you requested if other scripts or system components are also scanning.
BLE.Scanner
The Scanner object manages Bluetooth device discovery.
Constants
Scan Events:
BLE.Scanner.SCAN_START= 0BLE.Scanner.SCAN_STOP= 1BLE.Scanner.SCAN_RESULT= 2
Duration:
BLE.Scanner.INFINITE_SCAN= -1 (perpetual scanning)
BLE.Scanner.subscribe()
Subscribe to scan events.
Syntax:
BLE.Scanner.subscribe(callback[, userdata]) -> undefined
| Property | Type | Description | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| function or null | Function for scan events (null to unsubscribe)
| ||||||||||||||||||||||||||||||||||||||||||||||||
| any | Data to pass to callback |
Example:
BLE.Scanner.subscribe(function(event, result) {
switch(event) {
case BLE.Scanner.SCAN_START:
console.log("Scan started");
break;
case BLE.Scanner.SCAN_STOP:
console.log("Scan stopped");
break;
case BLE.Scanner.SCAN_RESULT:
console.log("Found device:", result.addr, "RSSI:", result.rssi);
if (result.local_name) {
console.log("Name:", result.local_name);
}
break;
}
});
BLE.Scanner.start()
Start scanning for Bluetooth devices with optional filters and configuration.
Syntax:
BLE.Scanner.start(options[, callback[, userdata]]) -> object or null
| Property | Type | Description |
|---|---|---|
| object | Scan configuration object |
| function | Optional event callback function |
| any | Data passed to callback |
Returns: Scan options object on success, or null if scan failed to start
For complete documentation of scan options and filters, see:
- Scan Options - Configure duration, interval, window, RSSI threshold
- Scan Filters - Filter by MAC address, name, services, manufacturer data (firmware 2.0.0+)
Basic Example
// Start 30-second scan with RSSI filtering
BLE.Scanner.start({
duration_ms: 30000,
active: true,
interval_ms: 100,
window_ms: 50,
rssi_thr: -70 // Only devices with RSSI > -70 dBm
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
console.log("Device:", result.addr, "RSSI:", result.rssi);
}
});
| Property | Type | Description | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| object | Scan configuration
| |||||||||||||||||||||
| function | Optional event callback | |||||||||||||||||||||
| any | Data for callback |
Returns: Scan options object or null if failed
Example:
// Start 30-second scan
BLE.Scanner.start({
duration_ms: 30000,
active: true,
interval_ms: 100,
window_ms: 50
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
console.log("Device:", result.addr);
}
});
// Continuous scan with RSSI filter
BLE.Scanner.start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
rssi_thr: -70 // Only devices with RSSI > -70 dBm
});
BLE.Scanner.stop()
Stop the current scan.
Syntax:
BLE.Scanner.stop() -> boolean
Returns: true if stopped successfully, false on error
BLE.Scanner.isRunning()
Check if a scan is active.
Syntax:
BLE.Scanner.isRunning() -> boolean
Returns: true if scanning, false otherwise
BLE.Scanner.getScanOptions()
Get current scan configuration.
Syntax:
BLE.Scanner.getScanOptions() -> object or null
Returns: Options object or null if not scanning
BLE.GAP
Generic Access Profile utilities for parsing advertisement data.
Constants
Address Types:
BLE.GAP.ADDRESS_TYPE_PUBLIC= 0x1BLE.GAP.ADDRESS_TYPE_RANDOM_STATIC= 0x2BLE.GAP.ADDRESS_TYPE_RANDOM_NON_RESOLVABLE= 0x3BLE.GAP.ADDRESS_TYPE_RANDOM_RESOLVABLE= 0x4
EIR Types:
BLE.GAP.EIR_FLAGS= 0x1BLE.GAP.EIR_SERVICE_16= 0x3BLE.GAP.EIR_SERVICE_128= 0x7BLE.GAP.EIR_SHORT_NAME= 0x8BLE.GAP.EIR_FULL_NAME= 0x9BLE.GAP.EIR_TX_POWER_LEVEL= 0xABLE.GAP.EIR_SERVICE_DATA_16= 0x16BLE.GAP.EIR_MANUFACTURER_SPECIFIC_DATA= 0xFF
BLE.GAP.parseName()
Extract device name from advertisement data.
Syntax:
BLE.GAP.parseName(data) -> string or null
| Property | Type | Description |
|---|---|---|
| string | Advertisement data to parse |
Returns: Device name string, or null if not found
BLE.GAP.parseManufacturerData()
Extract manufacturer-specific data from advertisement data.
Syntax:
BLE.GAP.parseManufacturerData(data) -> object or null
| Property | Type | Description |
|---|---|---|
| string | Advertisement data to parse |
Returns: Object containing manufacturer data, or null if not found
BLE.GAP.parseServiceData()
Extract data for a specific service UUID from advertisement data.
Syntax:
BLE.GAP.parseServiceData(data, uuid) -> string or null
| Property | Type | Description |
|---|---|---|
| string | Advertisement data to parse |
| string | Service UUID to look for |
Returns: Service data string, or null if not found
BLE.GAP.hasService()
Check if a service UUID is advertised in the advertisement data.
Syntax:
BLE.GAP.hasService(data, uuid) -> boolean
| Property | Type | Description |
|---|---|---|
| string | Advertisement data to check |
| string | Service UUID to look for |
Returns: true if the service UUID is present, false otherwise
Advertising support
Since version 2.0.0
Scripts can broadcast custom BLE advertisements, allowing Shelly devices to act as BLE beacons or peripheral devices. This enables use cases such as:
- Broadcasting sensor data to nearby BLE scanners
- Implementing custom BLE beacon protocols
- Advertising BTHome data for integration with home automation systems
- Creating location-aware applications using BLE proximity
Bluetooth advertising from scripts is available on:
- All Gen3 and Gen4 mains-powered devices that support scripting
- Select Gen2 mains-powered devices that support scripting
- Not supported on Gen 4 devices in Zigbee mode.
- Not supported on Battery-operated devices (e.g., Shelly Flood Gen4, Shelly Smoke Alarm).
Advertisement data must comply with BLE specification limits (maximum 31 bytes for advertisement data and 31 bytes for scan response data).
BLE.AdvBuilder
The BLE.AdvBuilder object provides methods for constructing BLE advertisement data and scan response payloads. It maintains internal state and allows building payloads incrementally by adding various data types.
The BLE.AdvBuilder is a singleton object within each script. Always call reset() before building a new payload to clear previous state.
BLE.AdvBuilder.addName()
Adds the device name to the advertisement data.
Syntax:
BLE.AdvBuilder.addName(name[, full]) -> boolean
| Property | Type | Description |
|---|---|---|
| string | The device name to advertise |
| boolean | If |
Returns: true if the name was added successfully, false if adding the name would exceed the maximum advertisement data size.
BLE.AdvBuilder.addShellyManufacturerData()
Adds Shelly-specific manufacturer data to the advertisement. Only the Allterco manufacturer ID (0x0BA9) is allowed.
Syntax:
BLE.AdvBuilder.addShellyManufacturerData(data) -> boolean
| Property | Type | Description |
|---|---|---|
| string | Manufacturer-specific data payload (without the manufacturer ID prefix) |
Returns: true if the manufacturer data was added successfully, false if adding the data would exceed the maximum advertisement data size.
The Allterco manufacturer ID (0x0BA9) is automatically prepended to the data. Only provide the manufacturer-specific payload.
BLE.AdvBuilder.addServiceData()
Adds service data for a specific service UUID to the advertisement.
Syntax:
BLE.AdvBuilder.addServiceData(uuid, data) -> boolean
| Property | Type | Description |
|---|---|---|
| string | Service UUID (16-bit, 32-bit, or 128-bit format) |
| string | Service-specific data payload |
Returns: true if the service data was added successfully, false if adding the data would exceed the maximum advertisement data size.
BLE.AdvBuilder.addBTHomeServiceData()
Adds BTHome service data to the advertisement. This is a convenience method that automatically uses the BTHome service UUID (0xFCD2).
Syntax:
BLE.AdvBuilder.addBTHomeServiceData(data) -> boolean
| Property | Type | Description |
|---|---|---|
| string | BTHome payload (should follow BTHome format, can be built with |
Returns: true if the BTHome service data was added successfully, false if adding the data would exceed the maximum advertisement data size.
This method is designed to work seamlessly with BTHome.DataBuilder. Build your BTHome payload first, then add it to the advertisement.
BLE.AdvBuilder.build()
Builds and returns the complete advertisement data as a string.
Syntax:
BLE.AdvBuilder.build() -> string
Returns: The constructed advertisement data as a string, ready to be used with BLE.advertiseOnce().
BLE.AdvBuilder.reset()
Resets the builder state, clearing all added data.
Syntax:
BLE.AdvBuilder.reset() -> undefined
Always call reset() before building a new advertisement payload to ensure a clean state.
BLE.advertiseOnce()
Triggers a 3-second broadcast of the provided advertisement data, immediately preempting any currently active advertisement.
Syntax:
BLE.advertiseOnce(adv_data[, scan_rsp]) -> boolean
| Property | Type | Description |
|---|---|---|
| string | Advertisement data (typically built with |
| string | Scan response data (typically built with |
Returns: true if the advertisement was successfully queued for broadcast, false if the data is invalid, exceeds size limits, or there is an error.
The advertisement will broadcast for exactly 3 seconds. Advertising is shared system-wide across all scripts. If multiple scripts attempt to broadcast simultaneously, the last script to call advertiseOnce() will preempt any currently active advertisement.
BTHome support
Since version 2.0.0
A global BTHome object provides utilities for parsing and building BTHome protocol data. BTHome is a BLE broadcast format for sensor data that is commonly used in home automation. Scripts can parse incoming BTHome advertisements and construct BTHome payloads for broadcasting.
BTHome support is only available on devices that include BTHome functionality.
BTHome.parseData()
Parses BTHome service data and returns an array of decoded values.
Syntax:
BTHome.parseData(data[, addr[, key]]) -> array or null
| Property | Type | Description |
|---|---|---|
| string | BTHome service data to parse |
| string | BTHome device address (required only if data is encrypted) Optional |
| string | AES encryption key as 128-bit hexadecimal string (required only if data is encrypted) Optional |
Returns: Array of BTHomeValue objects representing the decoded data, or null if parsing fails. If invoked with invalid arguments the script is aborted.
Each BTHomeValue object in the returned array has the following properties:
| Property | Type | Description |
|---|---|---|
| number | Value type constant (see BTHomeValue type constants below) |
| number | BTHome object ID |
| string | Human-readable name of the BTHome object |
| number | Object index (for multiple instances of the same object type) |
| number or boolean or string | The decoded value (type depends on the BTHome object) |
BTHomeValue type constants
BTHomeValue.UNKNOWN= 0BTHomeValue.SENSOR= 1BTHomeValue.BINARY_SENSOR= 2BTHomeValue.RAW_SENSOR= 3BTHomeValue.EVENT= 4BTHomeValue.OTHER= 5
BTHome.DataBuilder
The BTHome.DataBuilder object provides methods for constructing BTHome payloads. It maintains internal state and allows building payloads incrementally.
The BTHome.DataBuilder is a singleton object within each script. Always call reset() before building a new payload to clear previous state.
BTHome.DataBuilder.addObject()
Adds a BTHome object to the payload being built.
Syntax:
BTHome.DataBuilder.addObject(objID, value) -> boolean
| Property | Type | Description |
|---|---|---|
| number | BTHome object ID to add |
| number or boolean or string | Value for the object (type must match the object's expected type) |
Returns: true if the object was added successfully, false if the value type is invalid for the specified object ID.
BTHome object IDs and their expected value types are defined by the BTHome specification. Consult the specification for the complete list of supported object IDs. Text and raw BTHome objects have a maximum length of 3 characters/bytes in the current implementation.
BTHome.DataBuilder.setTriggerBased()
Marks the payload as trigger-based (event-driven) rather than state-based.
Syntax:
BTHome.DataBuilder.setTriggerBased() -> undefined
BTHome.DataBuilder.build()
Builds and returns the BTHome payload as an unencrypted string.
Syntax:
BTHome.DataBuilder.build() -> string
Returns: The constructed BTHome payload as a string.
BTHome.DataBuilder.buildEncrypted()
Builds and returns an encrypted BTHome payload.
Syntax:
BTHome.DataBuilder.buildEncrypted(key) -> string or null
| Property | Type | Description |
|---|---|---|
| string | AES encryption key as 128-bit hexadecimal string |
Returns: The constructed encrypted BTHome payload as a string, or null if encryption fails.
BTHome standard specifies AES CCM for encrypting BTHome data using device MAC address and a counter for ensuring uniqueness of the AES CCM IV. In this implementation the device bluetooth address and a global monotonic counter are used. The counter although rarely may wrap around and needs to be reset and also care should be taken not to reuse the same keys in this case. Facilities to manage the counter are provided in the global BTHome component, but it remains user responsibility.
BTHome.DataBuilder.reset()
Resets the builder state, clearing all added objects.
Syntax:
BTHome.DataBuilder.reset() -> undefined
Complete Examples
Temperature Sensor Monitor
// Monitor BLE temperature sensors
BLE.Scanner.start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: false
}, function(event, result) {
if (event !== BLE.Scanner.SCAN_RESULT) return;
// Check for temperature service
if (BLE.GAP.hasService(result.advData, "181a")) {
let serviceData = BLE.GAP.parseServiceData(result.advData, "181a");
if (serviceData.length >= 2) {
// Parse temperature (example format)
let temp = (serviceData.charCodeAt(0) | (serviceData.charCodeAt(1) << 8)) / 100;
console.log("Temperature from", result.addr, ":", temp, "°C");
}
}
});
iBeacon Scanner
// Scan for iBeacons
BLE.Scanner.subscribe(function(event, result) {
if (event !== BLE.Scanner.SCAN_RESULT) return;
let mfgData = BLE.GAP.parseManufacturerData(result.advData);
// Check for Apple iBeacon (0x004C)
if (mfgData.length >= 23 &&
mfgData.charCodeAt(0) === 0x4C &&
mfgData.charCodeAt(1) === 0x00) {
// Extract UUID (bytes 4-19)
let uuid = "";
for (let i = 4; i < 20; i++) {
uuid += ("0" + mfgData.charCodeAt(i).toString(16)).slice(-2);
if (i === 7 || i === 9 || i === 11 || i === 13) uuid += "-";
}
// Extract major/minor
let major = (mfgData.charCodeAt(20) << 8) | mfgData.charCodeAt(21);
let minor = (mfgData.charCodeAt(22) << 8) | mfgData.charCodeAt(23);
console.log("iBeacon:", uuid, "Major:", major, "Minor:", minor, "RSSI:", result.rssi);
}
});
BLE.Scanner.start({duration_ms: BLE.Scanner.INFINITE_SCAN});
Best Practices with Filters
Filter by MAC Address
// Efficiently scan for specific devices using filters (firmware 2.0.0+)
BLE.Scanner.start({
duration_ms: 60000,
active: false,
filters: [
{
addrs: ["AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66"]
}
]
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
console.log("Target device found:", result.addr);
console.log("RSSI:", result.rssi);
}
});
Filter by Device Name Pattern
// Find all Shelly BLU devices using name wildcard
BLE.Scanner.start({
duration_ms: 30000,
active: true, // Active scan needed to get device names
filters: [
{
name: "SBDW*" // Matches SBDW-002C, etc.
},
{
name: "SBHT*" // Matches SBHT-003C, etc.
}
]
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT && result.local_name) {
console.log("Found Shelly BLU device:", result.local_name);
}
});
Filter by Service UUID
// Find devices advertising specific services
BLE.Scanner.start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
rssi_thr: -80, // Only nearby devices
filters: [
{
services: ["181a"] // Environmental Sensing Service
},
{
services: ["180f"] // Battery Service
}
]
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
console.log("Device with service:", result.addr);
// Check which service was found
if (BLE.GAP.hasService(result.advData, "181a")) {
console.log("- Has Environmental Sensing");
}
if (BLE.GAP.hasService(result.advData, "180f")) {
console.log("- Has Battery Service");
}
}
});
Filter by Manufacturer Data
// Find Apple devices (iBeacons, AirTags, etc.)
BLE.Scanner.start({
duration_ms: 30000,
filters: [
{
manufacturerData: {
companyIdentifier: 0x004C // Apple Inc.
}
}
]
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
let mfgData = BLE.GAP.parseManufacturerData(result.advData);
if (mfgData && mfgData[0x004C]) {
console.log("Apple device found:", result.addr);
// Parse Apple-specific data
}
}
});
Combined Filters for BTHome Devices
// Scan for BTHome v2 devices with temperature data
BLE.Scanner.start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
rssi_thr: -75,
filters: [
{
serviceData: {
service: "fcd2", // BTHome service UUID
dataPrefix: "\x40\x02", // BTHome v2 + temperature object
mask: "\xFF\xFF" // Exact match for these bytes
}
}
]
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
let serviceData = BLE.GAP.parseServiceData(result.advData, "fcd2");
if (serviceData) {
// Parse BTHome temperature data
console.log("BTHome temperature sensor:", result.addr);
}
}
});
Scan Performance Optimization
Power-Efficient Scanning
// Low-power continuous monitoring
BLE.Scanner.start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: false, // Passive scan uses less power
interval_ms: 1000, // Longer interval
window_ms: 50, // Shorter window
rssi_thr: -70, // Filter weak signals
filters: [
{
services: ["181a"] // Only environmental sensors
}
]
});
High-Performance Scanning
// Fast device discovery
BLE.Scanner.start({
duration_ms: 10000,
active: true, // Get complete device info
interval_ms: 100, // Short interval
window_ms: 100, // Maximum window
rssi_thr: 0 // No RSSI filtering
});
Balanced Scanning for Multiple Devices
// Good balance for monitoring multiple device types
BLE.Scanner.start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: false,
interval_ms: 241, // Default balanced interval
window_ms: 61, // Default balanced window
rssi_thr: -80,
filters: [
{ name: "SBHT*" }, // Shelly BLU H&T
{ name: "SBDW*" }, // Shelly BLU Door/Window
{ services: ["181a"] }, // Generic temperature sensors
{ services: ["180f"] } // Battery-powered devices
]
}, function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
// Process different device types
if (result.local_name && result.local_name.startsWith("SBHT")) {
handleShellyHT(result);
} else if (BLE.GAP.hasService(result.advData, "181a")) {
handleGenericSensor(result);
}
}
});
function handleShellyHT(result) {
console.log("Shelly H&T:", result.addr);
}
function handleGenericSensor(result) {
console.log("Generic sensor:", result.addr);
}
Advertising Examples
Simple BTHome sensor data advertisement
BTHome.DataBuilder.reset();
BTHome.DataBuilder.addObject(0x02, 23.5); // Temperature
BTHome.DataBuilder.addObject(0x03, 65); // Humidity
let bthomePayload = BTHome.DataBuilder.build();
BLE.AdvBuilder.reset();
if (BLE.AdvBuilder.addBTHomeServiceData(bthomePayload)) {
let advData = BLE.AdvBuilder.build();
if (BLE.advertiseOnce(advData)) {
print("BTHome advertisement sent for 3 seconds");
}
}
Advertisement with device name and scan response
BLE.AdvBuilder.reset();
BLE.AdvBuilder.addServiceData("180f", "\x64"); // Battery service: 100%
let mainAdv = BLE.AdvBuilder.build();
BLE.AdvBuilder.reset();
BLE.AdvBuilder.addName("MyShelly", true);
let scanRsp = BLE.AdvBuilder.build();
BLE.advertiseOnce(mainAdv, scanRsp);
Periodic sensor beacon (every 30 seconds)
Timer.set(30000, true, function() {
let temp = Shelly.getComponentStatus("temperature", 0).tC;
BTHome.DataBuilder.reset();
BTHome.DataBuilder.addObject(0x02, temp);
let payload = BTHome.DataBuilder.build();
BLE.AdvBuilder.reset();
BLE.AdvBuilder.addBTHomeServiceData(payload);
let adv = BLE.AdvBuilder.build();
if (BLE.advertiseOnce(adv)) {
print("Temperature beacon sent:", temp, "°C");
}
});
Custom manufacturer data
BLE.AdvBuilder.reset();
let customData = "\x01\x02\x03\x04"; // Custom payload
if (BLE.AdvBuilder.addShellyManufacturerData(customData)) {
let adv = BLE.AdvBuilder.build();
BLE.advertiseOnce(adv);
}
BTHome Examples
Parse incoming BTHome data from a BLE scan
BLE.Scanner.subscribe(function(event, result) {
if (event === BLE.Scanner.SCAN_RESULT) {
// Check if this is a BTHome device
if (result.service_data && result.service_data["fcd2"]) {
let bthomeData = result.service_data["fcd2"];
// Parse the BTHome data
let values = BTHome.parseData(bthomeData);
if (values !== null) {
// Process each value
for (let i = 0; i < values.length; i++) {
let v = values[i];
print("BTHome object:", v.name, "=", v.val);
// Example: react to temperature reading
if (v.name === "temperature") {
print("Temperature:", v.val, "°C");
}
}
}
}
}
});
Build a BTHome payload
BTHome.DataBuilder.reset();
BTHome.DataBuilder.addObject(0x02, 23.5); // Temperature
BTHome.DataBuilder.addObject(0x03, 65); // Humidity
BTHome.DataBuilder.addObject(0x15, true); // Motion detected
// Get unencrypted payload
let payload = BTHome.DataBuilder.build();
print("BTHome payload:", btoh(payload));
// Or build encrypted payload
let encryptedPayload = BTHome.DataBuilder.buildEncrypted("0123456789abcdef0123456789abcdef");
if (encryptedPayload !== null) {
print("Encrypted payload:", btoh(encryptedPayload));
}
Performance Considerations
- BLE scanning is CPU and memory intensive
- Filter results to specific devices when possible
- Use passive scanning when active scanning isn't needed
- Consider using intervals for periodic scanning instead of continuous
- Results are deduplicated for 3 seconds to reduce load
Version Notes
- v0.12.0: Initial BLE support
- v1.0.0: 3-second deduplication added
- v1.5.0: Multiple script scan support
- v1.6.0: Bluetooth start/stop without reboot
- v1.7.0: CPU throttling at 25% usage
- v2.0.0: Advertising and BTHome support