SDM interceptors

Warning

Custom interceptors and their type contracts are still experimental. You can use logger, dp-kit, and matter-kit directly, but if you implement your own interceptors, expect possible API adjustments in future versions.

interceptors let you insert shared logic into SDM initialization, DP sending, and event callback chains. They are useful for logging, protocol conversion, structured state mapping, and conditional short-circuiting.

When do you need interceptors?

✅ Good fit:

  • You need shared logic, logging, or analytics before or after publishDps
  • You need to convert raw device reports into business-friendly data
  • You want to reuse logger, dp-kit, or matter-kit
  • Multiple pages depend on the same initialization or protocol processing logic

❌ Probably unnecessary:

  • The logic is only for one page and does not need to be reused
  • You only read simple DP features and useProps / useActions is enough
  • You do not need to change the SDM init, send, or event chains

How do you choose a built-in interceptor?

InterceptorBest forCore capabilityDocs
loggerDevelopment debuggingPrints logs for init / request / response chainslogger
dp-kitComplex DP featuresProtocol parsing, structured state, send options (sendOptions), optimistic UI updatesdp-kit
matter-kitMatter lighting devicesMatter ↔ standard DP conversion; can be composed with dp-kitmatter-kit
Info

SDM mounts logger on built-in chains by default. But once you manually configure an interceptor array for a specific chain, that chain is replaced by your new array. If you still want logs, add logger back explicitly.

Integration guide

Step 1: Choose the interception point

CategoryAvailable entriesTypical use
initinitDpState, initDevInfoAdjust dpState or devInfo during initialization
requestpublishDpsFormat outgoing data, append params, throttle or debounce sends
responseonDpDataChange, onDeviceInfoUpdated, onDeviceOnlineStatusUpdate, onNetworkStatusChange, onBluetoothAdapterStateChangeTransform reports, sync state, or enhance events
onInit(device) => voidExtra logic after device initialization, such as dpKit.init(device)

Step 2: Mount interceptors on SmartDeviceModel

src/devices/index.ts

import { SmartDeviceModel, createDpKit, logger } from '@ray-js/panel-sdk';
import { defaultSchema } from '@/devices/schema';

type SmartDeviceSchema = typeof defaultSchema;

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    immediate: true,
  },
});

const options = {
  interceptors: {
    onInit: (device) => {
      dpKit.init(device);
    },
    init: {
      initDpState: [logger, ...dpKit.interceptors.init.initDpState],
      initDevInfo: [logger, ...dpKit.interceptors.init.initDevInfo],
    },
    request: {
      publishDps: [logger, ...dpKit.interceptors.request.publishDps],
    },
    response: {
      onDpDataChange: [logger, ...dpKit.interceptors.response.onDpDataChange],
      onDeviceInfoUpdated: [logger, ...dpKit.interceptors.response.onDeviceInfoUpdated],
      onDeviceOnlineStatusUpdate: [logger],
      onNetworkStatusChange: [logger],
      onBluetoothAdapterStateChange: [logger],
    },
  },
};

export const devices = {
  common: new SmartDeviceModel<SmartDeviceSchema>(options),
};

If you only use one built-in kit, you can also pass interceptors: dpKit.interceptors or interceptors: matterKit.interceptors directly.

Step 3: Order multiple interceptors correctly

  • Interceptors in the same chain wrap in array order; earlier items receive the input first
  • If an interceptor does not call next(...), later interceptors and the underlying method will not run
  • When combining dp-kit and matter-kit for publishDps, dp-kit usually goes first and matter-kit second
request: {
  publishDps: [
    ...dpKit.interceptors.request.publishDps,
    ...matterKit.interceptors.request.publishDps,
  ],
},

Custom interceptors

Common types

TypeChainPurpose
InterceptorGenericA quick way to define a general-purpose interceptor
InitDpStateInterceptorinit.initDpStateShared processing around dpState initialization
InitDevInfoInterceptorinit.initDevInfoShared processing around devInfo initialization
PublishDpsInterceptorrequest.publishDpsRewrite outgoing params, append DP features, or change options
OnDpDataChangeInterceptorresponse.onDpDataChangeConvert raw device reports into business state
OnDeviceInfoUpdatedInterceptorresponse.onDeviceInfoUpdatedReact to device info updates
OnDeviceOnlineStatusUpdateInterceptorresponse.onDeviceOnlineStatusUpdateReact to online/offline updates
OnNetworkStatusChangeInterceptorresponse.onNetworkStatusChangeReact to network changes
OnBluetoothAdapterStateChangeInterceptorresponse.onBluetoothAdapterStateChangeReact to Bluetooth state changes

For group scenarios, there are also OnGroupDpDataChangeInterceptor and OnGroupInfoChangeInterceptor. This guide focuses on the most common single-device SDM chains.

Execution model

Info

Interceptors use a three-layer curried shape: ctx => next => data.

ParameterDescription
ctx.typeThe current chain name, such as publishDps or onDpDataChange
ctx.logThe logger for the current chain, supporting log.info / log.warn / log.fatal
ctx.instanceThe current SmartDeviceModel instance, which can access getDpState(), getDevInfo(), and more
nextThe next interceptor or the underlying method; the chain continues only if you call it
dataThe current request input or event payload
const interceptor = (ctx) => (next) => (data) => {
  return next(data);
};

Common patterns

1. Append or rewrite data before sending

import { PublishDpsInterceptor } from '@ray-js/panel-sdk';

export const appendExtraDp: PublishDpsInterceptor<SmartDeviceSchema> =
  (ctx) => (next) => (dpState, options) => {
    const nextDpState = {
      ...dpState,
      power_go: !ctx.instance.getDpState().power_go,
    };

    ctx.log.info('append extra dp before publishDps', nextDpState, 'appendExtraDp');

    return next(nextDpState, options);
  };

2. Convert raw reports into business data

import { DpState, DpValue, OnDpDataChangeInterceptor } from '@ray-js/panel-sdk';

export const mapDpsToDpState: OnDpDataChangeInterceptor<
  SmartDeviceSchema,
  DpState
> = (ctx) => (next) => (data) => {
  const devInfo = ctx.instance.getDevInfo();
  const dpState = {} as DpState;

  Object.keys(data.dps).forEach((dpId) => {
    dpState[devInfo.idCodes[dpId]] = data.dps[dpId] as DpValue;
  });

  return next(dpState);
};

3. Short-circuit the rest of the chain

import { Interceptor } from '@ray-js/panel-sdk';

export const skipWhenReadonly: Interceptor<SmartDeviceSchema> =
  (ctx) => (next) => (data) => {
    const isReadonlyMode = true;

    if (isReadonlyMode) {
      ctx.log.info('skip current interceptor chain', data, 'skipWhenReadonly');
      return null;
    }

    return next(data);
  };

Further reading

logger

Integration details and the type definition of the default logging interceptor

logger docs

dp-kit

Full guide to complex DP parsing, sendOptions, and structured hooks

dp-kit usage guide

matter-kit

Matter lighting DP mapping and composition with other interceptors

matter-kit usage guide

SmartDeviceModel

interceptors are ultimately mounted on SmartDeviceModel; see the API doc for the full initialization options

SmartDeviceModel init API