Skip to content

Commit

Permalink
Implemented manufacturer data parsing
Browse files Browse the repository at this point in the history
As the data format is different for each manufacturer, its decoding need to be implemented per vendor.
Currently supported one is Shelly as the majority of BTHome devices on the market are made by Shelly.
  • Loading branch information
byonchev committed Jan 26, 2025
1 parent 2eee33a commit 2afa43e
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 16 deletions.
26 changes: 24 additions & 2 deletions src/bluetooth/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Peripheral } from '@stoprocent/noble';
import { EventEmitter } from 'events';

import { BluetoothDevice, BluetoothError } from './types.js';
import { BluetoothDevice, ManufacturerData, BluetoothError } from './types.js';
import { wrapError } from '../util/errors.js';
import { decodeShellyManufacturerData } from './shelly.js';

export class BluetoothScanner {
private static readonly DISCOVER_EVENT = 'discover';
Expand Down Expand Up @@ -71,14 +72,35 @@ export class BluetoothScanner {

const mac = peripheral.address.toLowerCase();
const name = advertisementData.localName || this.generateDeviceName(mac);

const serviceData = service.data;
const manufacturerData = this.decodeManufacturerData(advertisementData.manufacturerData);

if (!manufacturerData.serialNumber) {
manufacturerData.serialNumber = mac;
}

const device : BluetoothDevice = { name, mac, serviceData };
const device : BluetoothDevice = { name, mac, serviceData, manufacturerData };

this.events.emit(BluetoothScanner.DISCOVER_EVENT, device);
}

private generateDeviceName(mac: string) {
return 'BLE ' + mac.replaceAll(':', '').slice(6).toUpperCase();
}

private decodeManufacturerData(data? : Buffer) : ManufacturerData {
if (!data) {
return {};
}

const companyIdentifier = data.readUInt16LE(0);

switch(companyIdentifier) {
case 0x0BA9:
return decodeShellyManufacturerData(data);
default:
return {};
}
}
}
44 changes: 44 additions & 0 deletions src/bluetooth/shelly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ManufacturerData } from './types';

function decodeModelName(identifier: number) : (string | undefined) {
switch (identifier) {
case 0x0001:
return 'Shelly BLU Button1';
case 0x0002:
return 'Shelly BLU DoorWindow';
case 0x0003:
return 'Shelly BLU HT';
case 0x0005:
return 'Shelly BLU Motion';
case 0x0006:
return 'Shelly BLU Wall Switch 4';
case 0x0007:
return 'Shelly BLU RC Button 4';
case 0x0008:
return 'Shelly BLU TRV';
}
}

export function decodeShellyManufacturerData(data: Buffer) : ManufacturerData {
const result : ManufacturerData = { manufacturer: 'Shelly' };

let offset = 2;
while (offset < data.length) {
const blockType = data[offset];

switch (blockType) {
case 0x01:
offset += 3;
break;
case 0x0A:
offset += 7;
break;
case 0x0B:
result.model = decodeModelName(data.readUInt16LE(offset + 1));
offset += 3;
break;
}
}

return result;
}
9 changes: 8 additions & 1 deletion src/bluetooth/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
export type BluetoothDevice = {
name: string,
mac: string,
serviceData: Buffer
serviceData: Buffer,
manufacturerData: ManufacturerData
};

export type ManufacturerData = {
manufacturer?: string,
model? : string,
serialNumber? : string
};

export class BluetoothError extends Error {}
23 changes: 19 additions & 4 deletions src/bthome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EventEmitter } from 'events';

import { BTHomeSensorData, BTHomeDecryptionError, BTHomeDecodingError, ButtonEvent } from './types.js';
import { wrapError } from '../util/errors.js';
import { ManufacturerData } from '../bluetooth/types.js';

type DecryptionResult = {
data: Buffer,
Expand All @@ -17,13 +18,15 @@ export class BTHomeDevice {
private static readonly UPDATE_EVENT = 'update';

private readonly mac: Buffer;
private readonly manufacturerData: ManufacturerData;
private readonly encryptionKey?: Buffer;
private readonly events : EventEmitter = new EventEmitter();

private lastSensorData?: BTHomeSensorData;
private lastSensorData?: BTHomeSensorData;

constructor(mac: string, encryptionKey?: string, initialPayload?: Buffer) {
constructor(mac: string, manufacturerData : ManufacturerData, encryptionKey?: string, initialPayload?: Buffer) {
this.mac = Buffer.from(mac.replaceAll(':', ''), 'hex');
this.manufacturerData = manufacturerData;
this.encryptionKey = encryptionKey?.length ? Buffer.from(encryptionKey, 'hex') : undefined;

if (initialPayload) {
Expand Down Expand Up @@ -60,6 +63,10 @@ export class BTHomeDevice {
return this.mac.toString('hex');
}

getManufacturerData() : ManufacturerData {
return Object.assign({}, this.manufacturerData);
}

private decodePayload(payload: Buffer): BTHomeSensorData {
const flags = payload.readUInt8(0);
const isEncrypted = (flags & 0x01) !== 0;
Expand Down Expand Up @@ -129,6 +136,16 @@ export class BTHomeDevice {
offset += 2;
break;

// Firmware version
case 0xF1:
result.firmwareVersion = `${data[offset+4]}.${data[offset+3]}.${data[offset+2]}.${data[offset+1]}`;
offset += 5;
break;
case 0xF2:
result.firmwareVersion = `${data[offset+3]}.${data[offset+2]}.${data[offset+1]}`;
offset += 4;
break;

// Battery
case 0x01:
result.battery = data.readUInt8(offset + 1);
Expand Down Expand Up @@ -245,7 +262,6 @@ export class BTHomeDevice {
case 0x04:
case 0x4B:
case 0x3C:
case 0xF2:
offset += 4;
break;
case 0x3E:
Expand All @@ -257,7 +273,6 @@ export class BTHomeDevice {
case 0x5B:
case 0x5C:
case 0x55:
case 0xF1:
offset += 5;
break;
case 0x53:
Expand Down
1 change: 1 addition & 0 deletions src/bthome/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type BTHomeSensorData = {
id?: number,
firmwareVersion?: string,
counter?: number,
temperature?: number,
humidity?: number,
Expand Down
4 changes: 2 additions & 2 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class BTHomePlatform implements DynamicPlatformPlugin {
if (accessory && !this.handles.has(uuid)) {
this.log.info('Restoring existing accessory from cache:', accessory.displayName);

accessory.context.device = new BTHomeDevice(mac, config.encryptionKey, device.serviceData);
accessory.context.device = new BTHomeDevice(mac, device.manufacturerData, config.encryptionKey, device.serviceData);

this.handles.set(uuid, new BTHomeAccessory(this, accessory));
}
Expand All @@ -84,7 +84,7 @@ export class BTHomePlatform implements DynamicPlatformPlugin {
this.log.info('Adding new accessory:', name);

accessory = new this.api.platformAccessory(name, uuid);
accessory.context.device = new BTHomeDevice(mac, config.encryptionKey, device.serviceData);
accessory.context.device = new BTHomeDevice(mac, device.manufacturerData, config.encryptionKey, device.serviceData);

this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);

Expand Down
28 changes: 21 additions & 7 deletions src/platformAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ export class BTHomeAccessory {
private readonly platform: BTHomePlatform,
private readonly accessory: PlatformAccessory,
) {
this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer')
.setCharacteristic(this.platform.Characteristic.Model, 'Default-Model')
.setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial');

const device : BTHomeDevice = this.getDevice();
const manufacturerData = device.getManufacturerData();

this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, manufacturerData.manufacturer || 'Unknown')
.setCharacteristic(this.platform.Characteristic.Model, manufacturerData.model || 'Unknown')
.setCharacteristic(this.platform.Characteristic.SerialNumber, manufacturerData.serialNumber || 'Unknown');

this.setupServices();

Expand Down Expand Up @@ -74,7 +75,7 @@ export class BTHomeAccessory {
return fallback;
}

/* Sensor specific characteristic and events begin here */
/* Characteristics related code begin here */

private setupServices() {
const device = this.getDevice();
Expand Down Expand Up @@ -164,6 +165,10 @@ export class BTHomeAccessory {
if (sensorData?.button) {
this.handleButtonEvent(sensorData.button);
}

if (sensorData?.firmwareVersion) {
this.updateFirmwareVersion(sensorData.firmwareVersion);
}
}

private getDevice() : BTHomeDevice {
Expand Down Expand Up @@ -222,5 +227,14 @@ export class BTHomeAccessory {
}
}

/* Sensor specific characteristic and events end here */
private updateFirmwareVersion(version: string) {
const service = this.accessory.getService(this.platform.Service.AccessoryInformation);
if (!service) {
return;
}

service.setCharacteristic(this.platform.Characteristic.FirmwareRevision, version);
}

/* Characteristics related code end here */
}

0 comments on commit 2afa43e

Please sign in to comment.