BLE characteristic subscribeToNotifications() method not working

I’m building an app for iTAG keyfinders. I built a test snippet to check if everything works, but subscribing to notifications failed. I then asked Claude to check my code for issues, but after many attempts it still didn’t work. The error message:

Error: Cannot read property 'characteristics' of undefined
    at Remote Process
    at HomeyClient.emit (/opt/homey-client/system/manager/ManagerApps/AppProcess/node_modules/@athombv/homey-apps-sdk-v3/lib/HomeyClient.js:1:312)
    at Object.emit (/opt/homey-client/system/manager/ManagerApps/AppProcess/node_modules/@athombv/homey-apps-sdk-v3/manager/ble.js:116:32)
    at Object.emit (/opt/homey-client/system/manager/ManagerApps/AppProcess/node_modules/@athombv/homey-apps-sdk-v3/lib/BlePeripheral.js:1:3125)
    at Object.emit (/opt/homey-client/system/manager/ManagerApps/AppProcess/node_modules/@athombv/homey-apps-sdk-v3/lib/BleService.js:1:1409)
    at BleCharacteristic.subscribeToNotifications (/opt/homey-client/system/manager/ManagerApps/AppProcess/node_modules/@athombv/homey-apps-sdk-v3/lib/BleCharacteristic.js:1:1303) 
    at iTAGDevice.onInit (/drivers/itag/device.js:89:24)    
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async iTAGDevice._onInit (/opt/homey-client/system/manager/ManagerApps/AppProcess/node_modules/@athombv/homey-apps-sdk-v3/lib/Device.js:1:1743)

I decided to add some logging, and found that “id“ of the characteristic buttonChar was undefined. Claude then gave me a workaround, but it still didn’t work. It then said that it’s a bug in the Homey Apps SDK, but I don’t think so since there are no reports about it online.

I decided to check some examples online, but couldn’t find any that used notifications (I clicked the “Bluetooth LE“ category in the Appstore and checked through the GitHub repos listed there). I was able to find a few examples, but they worked in the same way that I already tried. Could it be something with the iTAG device itself? Disabling the “LinkLost“ alarm works fine.

My full code:

'use strict';

const Homey = require('homey');

module.exports = class iTAGDevice extends Homey.Device {

  /**
   * onInit is called when the device is initialized.
   */
  async onInit() {
    this.log('iTAG device has been initialized');
    this.log(`Homey version: ${this.homey.version}`);
    
    // Only try notifications if Homey >= 6.0
    if (parseInt(this.homey.version, 10) < 6) {
      this.log('Homey version does not support BLE notifications');
      return;
    }
    
    try {
      const address = this.getStoreValue('address');
      const uuid = address.replace(/:/g, '').toLowerCase();
      
      this.log('Finding iTAG device...');
      const advertisement = await this.homey.ble.find(uuid);
      
      this.log('Connecting to iTAG device...');
      const peripheral = await advertisement.connect();
      this.log('Connected successfully');
      
      // Register the peripheral for notifications
      this.log('Registering peripheral for notifications...');
      this.homey.ble.__registerPeripheral(peripheral);
      this.log('Peripheral registered');
      
      // Discover all services first
      this.log('Discovering all services...');
      const services = await peripheral.discoverServices();
      this.log('Discovered', services.length, 'services');
      
      // Find the FFE0 service from the discovered services
      const service = services.find(s => s.uuid === '0000ffe000001000800000805f9b34fb');
      if (!service) {
        throw new Error('FFE0 service not found');
      }
      this.log('Found FFE0 service, ID:', service.id);
      
      // WORKAROUND: If service.id is undefined, manually set it to the UUID
      if (!service.id) {
        this.log('WARNING: service.id is undefined, manually setting it');
        service.id = service.uuid;
      }
      
      // Now discover characteristics for THIS specific service
      this.log('Discovering characteristics for FFE0 service...');
      const characteristics = await service.discoverCharacteristics();
      this.log('Discovered', characteristics.length, 'characteristics');
      
      // Find the characteristics we need
      const linkLostChar = characteristics.find(c => c.uuid === '0000ffe200001000800000805f9b34fb');
      const buttonChar = characteristics.find(c => c.uuid === '0000ffe100001000800000805f9b34fb');
      
      if (!linkLostChar) {
        throw new Error('LinkLost characteristic (FFE2) not found');
      }
      if (!buttonChar) {
        throw new Error('Button characteristic (FFE1) not found');
      }
      
      // WORKAROUND: Set IDs for characteristics if they're undefined
      if (!linkLostChar.id) {
        linkLostChar.id = linkLostChar.uuid;
      }
      if (!buttonChar.id) {
        buttonChar.id = buttonChar.uuid;
      }

      this.log('Button char', buttonChar);
      
      this.log('Found both characteristics');
      
      // Write to LinkLost characteristic to disable alarm
      this.log('Writing to LinkLost characteristic to disable alarm...');
      await linkLostChar.write(Buffer.from([0x00]));
      this.log('Successfully disabled LinkLost alarm');
      
      // Subscribe to notifications
      this.log('Subscribing to button notifications...');
      await buttonChar.subscribeToNotifications((data) => {
        this.log('Received notification from iTAG:', data);
        if (data && data.length > 0) {
          this.log('Button data:', Array.from(data));
          if (data[0] === 0xFF) {
            this.log('Button pressed!');
            // Trigger a flow card here if needed
          }
        }
      });
      this.log('Successfully subscribed to notifications');
      
      // Store the peripheral for later use
      this.peripheral = peripheral;
      
      // Listen for disconnects
      peripheral.once('disconnect', () => {
        this.log('iTAG device disconnected');
        // Unregister the peripheral
        this.homey.ble.__unregisterPeripheral(peripheral);
        this.peripheral = null;
        this.reconnect();
      });
      
    } catch (error) {
      this.log('Error connecting to iTAG device during onInit:', error);
      this.log('Error message:', error.message);
      this.log('Error stack:', error.stack);
      
      // Try to reconnect after a delay
      this.reconnectTimeout = this.homey.setTimeout(() => {
        this.log('Attempting reconnect after error...');
        this.onInit();
      }, 30000);
    }
  }

  async reconnect() {
    this.log('Attempting to reconnect...');
    this.reconnectTimeout = this.homey.setTimeout(() => {
      this.onInit();
    }, 10000);
  }

  async onAdded() {
    this.log('iTAG device has been added');
  }

  async onSettings({ oldSettings, newSettings, changedKeys }) {
    this.log('iTAG device settings were changed');
  }

  async onRenamed(name) {
    this.log('iTAG device was renamed');
  }

  async onDeleted() {
    this.log('iTAG device has been deleted');
    
    if (this.reconnectTimeout) {
      this.homey.clearTimeout(this.reconnectTimeout);
    }
    
    if (this.peripheral) {
      try {
        this.homey.ble.__unregisterPeripheral(this.peripheral);
        await this.peripheral.disconnect();
      } catch (error) {
        this.log('Error disconnecting:', error);
      }
    }
  }

};

I already tried with the Homey Developer Tools and there the notifications do come through.

I also tried it this way:




const Homey = require('homey');



module.exports = class iTAGDevice extends Homey.Device {



  /\*\*

   \* onInit is called when the device is initialized.

   \*/

  async onInit() {

    this.log('iTAG device has been initialized');

    this.log(\`Homey version: ${this.homey.version}\`);

    

    // Only try notifications if Homey >= 6.0

    if (parseInt(this.homey.version, 10) < 6) {

      this.log('Homey version does not support BLE notifications');

      return;

    }

    

    try {

      const address = this.getStoreValue('address');

      const uuid = address.replace(/:/g, '').toLowerCase();

      

      this.log('Finding iTAG device...');

      const advertisement = await this.homey.ble.find(uuid);

      

      this.log('Connecting to iTAG device...');

      const peripheral = await advertisement.connect();

      this.log('Connected successfully');

      

      // Register the peripheral for notifications

      this.log('Registering peripheral for notifications...');

      this.homey.ble.\__registerPeripheral(peripheral);

      this.log('Peripheral registered');

      

      // Discover all services first

      this.log('Discovering all services...');

      const services = await peripheral.discoverServices();

      this.log('Discovered', services.length, 'services');

      

      // Find the FFE0 service from the discovered services

      const service = await peripheral.getService('0000ffe000001000800000805f9b34fb');

      if (!service) {

        throw new Error('FFE0 service not found');

      }

      this.log('Found FFE0 service, ID:', service.id);

      

      // WORKAROUND: If service.id is undefined, manually set it to the UUID

      if (!service.id) {

        this.log('WARNING: service.id is undefined, manually setting it');

        service.id = service.uuid;

      }

      

      

      // Find the characteristics we need

      const linkLostChar = await service.getCharacteristic('0000ffe200001000800000805f9b34fb');

      const buttonChar = await service.getCharacteristic('0000ffe100001000800000805f9b34fb');

      

      if (!linkLostChar) {

        throw new Error('LinkLost characteristic (FFE2) not found');

      }

      if (!buttonChar) {

        throw new Error('Button characteristic (FFE1) not found');

      }

      

      // WORKAROUND: Set IDs for characteristics if they're undefined

      if (!linkLostChar.id) {

        linkLostChar.id = linkLostChar.uuid;

      }

      if (!buttonChar.id) {

        buttonChar.id = buttonChar.uuid;

      }



      this.log('Button char', buttonChar);

      

      this.log('Found both characteristics');

      

      // Write to LinkLost characteristic to disable alarm

      this.log('Writing to LinkLost characteristic to disable alarm...');

      await linkLostChar.write(Buffer.from(\[0x00\]));

      this.log('Successfully disabled LinkLost alarm');

      

      // Subscribe to notifications

      this.log('Subscribing to button notifications...');

      await buttonChar.subscribeToNotifications((data) => {

        this.log('Received notification from iTAG:', data);

        if (data && data.length > 0) {

          this.log('Button data:', Array.from(data));

          if (data\[0\] === 0xFF) {

            this.log('Button pressed!');

            // Trigger a flow card here if needed

          }

        }

      });

      this.log('Successfully subscribed to notifications');

      

      // Store the peripheral for later use

      this.peripheral = peripheral;

      

      // Listen for disconnects

      peripheral.once('disconnect', () => {

        this.log('iTAG device disconnected');

        // Unregister the peripheral

        this.homey.ble.\__unregisterPeripheral(peripheral);

        this.peripheral = null;

        this.reconnect();

      });

      

    } catch (error) {

      this.log('Error connecting to iTAG device during onInit:', error);

      this.log('Error message:', error.message);

      this.log('Error stack:', error.stack);

      

      // Try to reconnect after a delay

      this.reconnectTimeout = this.homey.setTimeout(() => {

        this.log('Attempting reconnect after error...');

        this.onInit();

      }, 30000);

    }

  }



  async reconnect() {

    this.log('Attempting to reconnect...');

    this.reconnectTimeout = this.homey.setTimeout(() => {

      this.onInit();

    }, 10000);

  }



  async onAdded() {

    this.log('iTAG device has been added');

  }



  async onSettings({ oldSettings, newSettings, changedKeys }) {

    this.log('iTAG device settings were changed');

  }



  async onRenamed(name) {

    this.log('iTAG device was renamed');

  }



  async onDeleted() {

    this.log('iTAG device has been deleted');

    

    if (this.reconnectTimeout) {

      this.homey.clearTimeout(this.reconnectTimeout);

    }

    

    if (this.peripheral) {

      try {

        this.homey.ble.\__unregisterPeripheral(this.peripheral);

        await this.peripheral.disconnect();

      } catch (error) {

        this.log('Error disconnecting:', error);

      }

    }

  }



};```

Does anyone know of good examples with BLE notifications? Is this a known issue?

But that gets the same error message. I found this example on GitHub which appears to do the exact same thing as I’m doing:

Is it a known issue? Are there any other examples with BLE notifications? Or am I doing something wrong here?

I use a Homey Pro 2019 with FW v12.7.1