dp-kit

Info

dp-kit added since @ray-js/panel-sdk@1.7.0

A lightweight toolkit for handling device-panel DP features, with built-in parsing for complex DP features, send option enhancements (sendOptions), and optimistic UI updates for complex DP scenarios.

When do you need dp-kit?

✅ Good fit:

  • Your product has complex DP features, such as raw/string payloads like colour_data that need to be parsed into hue / saturation / value
  • You need throttling, debouncing, or delayed DP sending
  • You want to update the UI immediately after sending (immediate mode)
  • You need to ignore device report responses and synchronize DP state from the cloud
  • You need custom logic before or after sending (onBeforeSendDp / onAfterSendDp)

❌ Probably unnecessary:

  • Your product only uses simple DP feature types (bool / enum / value), and useProps + useActions is enough
  • You do not need any send-time enhancements

Integration Guide

Step 1: Configure the product DP schema file

src/devices/schema.ts

See Creating SDM

Step 2: Configure the complex DP parsing file (Optional)

src/devices/protocols.ts

  • This step maps to the protocols field in createDpKit and defines how complex DP payloads are parsed and formatted. Skip it if your product does not use complex DP data.
export const protocols = {
  colour_data: {
    parser: (dpValue) => ({
      hue: parseInt(dpValue.slice(0, 4), 16),
      saturation: parseInt(dpValue.slice(4, 8), 16),
      value: parseInt(dpValue.slice(8, 12), 16),
    }),
    formatter: ({ hue, saturation, value }) =>
      `${hue.toString(16).padStart(4, '0')}${saturation
        .toString(16)
        .padStart(4, '0')}${value.toString(16).padStart(4, '0')}`,
  },
};

Step 3: Create dp-kit for your scenario

src/devices/index.ts

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

type SmartDeviceSchema = typeof defaultSchema;

export const dpKit = createDpKit<SmartDeviceSchema>({ protocols });

const options = {
  interceptors: dpKit.interceptors,
};

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

Step 4: Initialize complex DP protocol data (Optional)

src/app.tsx

  • Integrate dpKit.init to initialize complex DP protocol data. You can skip this if you do not use structured DP data.

Step 5: Integrate global type definitions (Optional)

typings/sdm.d.ts

  • Refer to the latest template file: PublicSdmTemplate/typings/sdm.d.ts
  • Replace the template imports such as @/devices/schema and @/devices/protocols with the actual paths in your project
  • Skip this step if you do not need TS IntelliSense

Step 6: Read and send complex DP protocol data

src/pages/home/index.tsx

  • Use useStructuredProps to read structured DP data
  • Use useStructuredActions to send structured DP data
import _ from 'lodash-es';
import React from 'react';
import { View } from '@ray-js/ray';
import { useStructuredProps, useStructuredActions } from '@ray-js/panel-sdk';

export default function Home() {
  const colour = useStructuredProps((props) => props.colour_data);
  const actions = useStructuredActions();
  return (
    <View
      style={{ flex: 1 }}
      onClick={() => {
        actions.colour_data.set({
          hue: _.random(0, 360),
          saturation: _.random(0, 1000),
          value: _.random(0, 1000),
        });
      }}
    >
      <View>hue: {colour.hue}</View>
      <View>saturation: {colour.saturation}</View>
      <View>value: {colour.value}</View>
    </View>
  );
}

API

For the complete createDpKit API reference and full type definitions, see createDpKit API.

The sections below focus on how each option is used in practice.

Complex DP parsing (Optional)

This section maps to the protocols field in createDpKit. Use it to describe how complex DP features should be parsed and formatted, so your business code no longer needs to deal with raw payload conversions.

const dpKit: Middleware = createDpKit<SmartDeviceSchema>({
  protocols: {
    colour_data: {
      parser: (dpValue) => ({
        hue: parseInt(dpValue.slice(0, 4), 16),
        saturation: parseInt(dpValue.slice(4, 8), 16),
        brightness: parseInt(dpValue.slice(8, 12), 16),
      }),
      formatter: ({ hue, saturation, brightness }) =>
        `${hue.toString(16).padStart(4, '0')}${saturation
          .toString(16)
          .padStart(4, '0')}${brightness.toString(16).padStart(4, '0')}`,
    },

    custom_raw_dp: {
      parser: (dpValue) => {
        // Custom parsing
      },

      formatter: (dpValue) => {
        // Custom formatting
      },
    },
  },
});

/**
 * Read colour_data
 * useProps(props => props.colour_data) = 000003e803e8
 * useStructuredProps(props => props.colour_data) = { hue: 0, saturation: 1000, brightness: 1000 }
 */

/**
 * Send colour_data
 * const actions = useActions(); => actions.colour_data.set('000003e803e8');
 * const actions = useStructuredActions(); => actions.colour_data.set({ hue: 0, saturation: 1000, brightness: 1000 });
 */

If the payload shape is completely fixed, you can still use a shorter mapping-style config, but parser / formatter is usually easier to understand in tutorials.

Send options (sendOptions) (Optional)

These are the options used when sending DP features. In createDpKit, they correspond to the sendDpOption field and become the default behavior for later send calls.

For the full type definition, see createDpKit API > SendDpOption

PropertyTypeDefaultDescription
immediatebooleanfalseUpdate the UI immediately after sending, without waiting for a device report
throttlenumber0Send throttling in ms
debouncenumber0Send debouncing in ms; conflicts with throttle
delaynumber0Delay sending in ms
checkRepeatbooleanfalseSkip repeated values
orderedbooleanfalseSend multiple DP features in order
ignoreDpDataResponseboolean | IgnoreDpChangeInterceptorOptionsfalseIgnore device report responses (1.11.0+)
synchronizeDevPropertyboolean | DevPropInterceptorOptionsfalseSynchronize DP state with cloud device properties (1.11.0+)
protocolsRecord<string, CustomRawDpMap>-Temporary protocol conversion for a single send

ignoreDpDataResponse object fields:

PropertyTypeDefaultDescription
whiteDpCodesstring[]-Whitelisted DP features that should still respond
timeoutnumber10000Debounce timeout in ms; dpDataChange is triggered only after timeout (1.14.0+)
customRulefunction-Custom ignore rule with the highest priority (1.14.0+)
debugboolean-Enable debug logs

synchronizeDevProperty object fields:

PropertyTypeDescription
blackDpCodesstring[]Blacklisted DP features that should not be synchronized to the cloud
defaultStateobject | functionDefault value when cloud data is empty; async is supported

For example, if every send in your application should use a 600 ms throttle:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    throttle: 600,
  },
});

Or, if you want a DP feature to update immediately without waiting for a device report:

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

With immediate, the DP feature data injected by dp-kit will carry an extra __from__: dp-kit marker in SDM's internal onDataChange flow. This helps distinguish optimistic updates from real device reports. The original onDpDataChange event is not triggered.

If you only want to override a single send without waiting for a report:

const actions = useActions();
actions.power.toggle({ immediate: true });

Using only immediate still triggers one more update when the device report arrives, which can cause UI flicker. If you want to ignore the device report response entirely:

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

If some DP features should still respond, use a whitelist:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    immediate: true,
    ignoreDpDataResponse: {
      whiteDpCodes: ['switch'],
    },
  },
});

You can also configure a timeout:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    immediate: true,
    ignoreDpDataResponse: {
      timeout: 1000 * 5,
    },
  },
});

Or define a custom rule with the highest priority:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    immediate: true,
    ignoreDpDataResponse: {
      customRule: (dpState, lastSendTimestamp) => {
        if (dpState.switch !== undefined) {
          return true;
        }
        if (lastSendTimestamp === null) {
          return true;
        }
        const timeDiff = Date.now() - lastSendTimestamp;
        return timeDiff > 8000;
      },
    },
  },
});

When device report responses are ignored, the previous state will not be available after exiting and re-entering the panel. Use synchronizeDevProperty to restore DP state from the cloud:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    immediate: true,
    ignoreDpDataResponse: {
      whiteDpCodes: ['switch'],
    },
    synchronizeDevProperty: true,
  },
});

Every send writes the DP state into cloud device properties. When the panel opens again, dp-kit performs one initialization pass and restores the DP state from the cloud.

You can also exclude specific DP features from synchronization:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    synchronizeDevProperty: {
      blackDpCodes: ['switch'],
    },
  },
});

When cloud data is empty, you can define a default state:

const dpKit = createDpKit<SmartDeviceSchema>({
  sendDpOption: {
    ignoreDpDataResponse: true,
    synchronizeDevProperty: {
      defaultState: {
        switch: false,
      },
    },
  },
});

onBeforeSendDp / onAfterSendDp (Optional)

Hooks that run before and after publishDps. Passing SmartDeviceSchema is recommended so the hook receives the correct type.

export const dpKit = createDpKit<SmartDeviceSchema>({
  onBeforeSendDp(dpState) {
    console.log('=== onBeforeSendDp', dpState);
  },
  onAfterSendDp(dpState) {
    console.log('=== onAfterSendDp', dpState);
  },
});

Further Reading

logger interceptor

dp-kit ships well with the built-in logger interceptor for inspecting DP sends and reports in the console.

logger docs

matter-kit interceptor

If you also need to handle Matter devices, pair dp-kit with matter-kit.

matter-kit docs

SmartDeviceModel guide

dp-kit is built on top of the SmartDeviceModel interceptor mechanism.

SmartDeviceModel docs

Using SDM interceptors and custom DP protocol parsing

A tutorial showing custom interceptors and custom protocol parsing built on top of dp-kit.

Tutorial

Beacon panel template

A tutorial that builds a Beacon panel with dp-kit built-in capabilities.

Tutorial