Trouble Integration (Dispense SDK)

The Dispense SDK integrates with KOS’s trouble system to provide automatic trouble tracking on hardware models. This guide covers Dispense SDK-specific trouble integration patterns.

For comprehensive trouble system documentation including resolution operations, bulk operations, and trouble handlers, see the Trouble System Integration guide.

TroubleAware Decorator

Hardware models in the Dispense SDK use the @kosTroubleAware() decorator to automatically receive trouble notifications.

What It Provides

The decorator adds:

  • Automatic TroubleContainer dependency injection
  • Trouble filtering based on model path
  • Trouble grouping by type via troublesByType
  • Pour deferral logic via shouldDefer

Dispense SDK Models Using TroubleAware

  • HolderModel – Tracks holder-related troubles
  • PumpModel – Tracks pump-related troubles
  • BoardModel – Tracks board-related troubles

Accessing Troubles on Models

Models decorated with @kosTroubleAware() expose trouble information:

troubles Property

Array of all troubles affecting the model:

const holder = assembly.holderContainer.getModel("holder-path");
// Access all troubles for this holder
holder.troubles.forEach((trouble) => {
  console.log(trouble.title); // User-friendly title
  console.log(trouble.subtitle); // Additional detail
  console.log(trouble.type); // Trouble type identifier
  console.log(trouble.resolvable); // Can be resolved programmatically
  console.log(trouble.color); // UI color indicator
  console.log(trouble.rank); // Severity ranking
});

troublesByType Property

Troubles indexed by type for quick access:

const holder = assembly.holderContainer.getModel("holder-path");
// Check for specific trouble types
const enjoyByTroubles = holder.troublesByType["EnjoyByTrouble"] || [];
const agitationTroubles =
  holder.troublesByType["InsufficientAgitationTrouble"] || [];
if (enjoyByTroubles.length > 0) {
  console.log("Ingredient approaching expiration date");
}

shouldDefer Property

Boolean indicating if operations should be blocked:

const holder = assembly.holderContainer.getModel("holder-path");
if (holder.shouldDefer) {
  console.warn("Cannot pour - holder has blocking troubles");
  return;
}
// Safe to pour
await holder.performIntent({ intent: "PRIME" });

HolderModel Convenience Methods

HolderModel provides trouble-specific helpers:

const holder = assembly.holderContainer.getModel("holder-path");
// Quick check for enjoy-by warnings
if (holder.hasEnjoyByWarning) {
  showWarningDialog("Ingredient expiring soon");
}
// Check pour capability (abstracts trouble blocking logic)
if (holder.canPour) {
  await holder.performIntent({ intent: "CALIBRATE" });
}

SDK Trouble Mappers

The SDK registers extension points to connect device troubles to models.

EXTENSION_TROUBLE_MAPPER

Maps troubles to model paths based on the trouble’s iface (interface).

SDK Registrations: The Dispense SDK automatically registers mappers for:

  • "holder" – Maps holder troubles to HolderModel instances
  • "pump" – Maps pump troubles to PumpModel instances
  • "board" – Maps board troubles to BoardModel instances
  • "container" – Maps container troubles to IngredientContainer

When to add custom mappers: Only needed when introducing new trouble ifaces not already handled by the SDK.

Example for custom iface:

import {
  ExtensionManager,
  EXTENSION_TROUBLE_MAPPER,
} from "@kosdev-code/kos-ui-sdk";
ExtensionManager[EXTENSION_TROUBLE_MAPPER].register(
  "customComponent",
  async (trouble: TroubleModel<CustomTroubleData>) => {
    const result: string[] = [];
    result.push(...trouble.data.componentPaths);
    return result; // Model paths affected by this trouble
  }
);

EXTENSION_TROUBLE_DATA_MAPPER

Enriches trouble data with display-friendly context.

Purpose: Transforms or augments trouble data for UI display. Common use cases:

  • Looking up related models to add names (e.g., ingredientId → ingredientName)
  • Formatting data (e.g., timestamps, units)
  • Adding computed values
  • Providing UI-specific context

How it works:

  1. Mappers registered for each of the trouble’s ifaces execute first
  2. Mappers registered for the specific trouble type execute second
  3. All returned data is merged into the trouble data

SDK Registrations: The Dispense SDK registers data mappers for:

  • "holder" iface – Adds holder names
  • "pump" iface – Adds pump and ingredient names
  • "container" iface – Adds ingredient names
  • "FirmwareUpdateInProgressTrouble" type – Adds board/device names

Example 1: Mapper for an iface (applied to all troubles with that iface):

import {
  ExtensionManager,
  EXTENSION_TROUBLE_DATA_MAPPER,
  getKosModelSync,
} from "@kosdev-code/kos-ui-sdk";
// Enrich all troubles with "container" iface
ExtensionManager[EXTENSION_TROUBLE_DATA_MAPPER].register(
  "container",
  async (trouble: TroubleModel<ContainerTroubleData>) => {
    // Container iface has ingredientId, but not ingredientName
    if (trouble.data.ingredients.length > 0) {
      const ingredientId = trouble.data.ingredients[0];
      const { model } = getKosModelSync<IngredientModel>(ingredientId);
      return {
        ingredientName: model?.displayName ?? (model?.name || ""),
      };
    }
    return {};
  }
);

Example 2: Mapper for a specific trouble type:

// Enrich specific trouble type with custom context
ExtensionManager[EXTENSION_TROUBLE_DATA_MAPPER].register(
  "FirmwareUpdateInProgressTrouble",
  async (trouble: TroubleModel) => {
    if (trouble.data?.deviceId) {
      const { model: boardModel } = getKosModelSync<BoardModel>(
        trouble.data.deviceId
      );
      return {
        devices: boardModel ? boardModel.name : "",
      };
    }
    return {};
  }
);

Registration Location

The SDK registers these mappers automatically when models are loaded:

Using Troubles in UI

Displaying Trouble Information

import { useHolder } from "@kosdev-code/kos-dispense-sdk/ui";
function HolderCard({ holderPath }: { holderPath: string }) {
  const holder = useHolder(holderPath);
  if (!holder) return null;
  return (
    <div>
      <h3>{holder.name}</h3>
      {holder.hasEnjoyByWarning && (
        <div className="warning">Ingredient expiring soon</div>
      )}
      {holder.troubles.map((trouble) => (
        <div key={trouble.id} className={`trouble ${trouble.color}`}>
          <strong>{trouble.title}</strong>
          <p>{trouble.subtitle}</p>
        </div>
      ))}
    </div>
  );
}

Understanding Trouble Propagation

Automatic Assignment

  1. Device publishes trouble via WebSocket to /kos/troubles topic
  2. TroubleContainer receives trouble and creates TroubleModel
  3. Framework executes registered EXTENSION_TROUBLE_MAPPER for the trouble’s iface
  4. Mapper returns array of affected model paths
  5. TroubleContainer assigns trouble to those models
  6. Models decorated with @kosTroubleAware() receive updates automatically

Real-time Updates

Troubles update via WebSocket:

  • New troubles appear immediately on affected models
  • Resolved troubles are removed automatically
  • troublesByType and shouldDefer update reactively

TroubleContainer Dependency

The decorator automatically adds TroubleContainer as a dependency – no manual configuration needed.

Best Practices

Use Abstraction Methods

Models provide abstraction methods like canPour and shouldDefer that encapsulate trouble logic:

const holder = assembly.holderContainer.getModel("holder-path");
// Use abstraction - don't assume why
if (holder.canPour) {
  await holder.performIntent({ intent: "PRIME" });
}

Display All Relevant Troubles

Show both blocking and warning troubles for context:

const allTroubles = holder.troubles;
const blockingTroubles = allTroubles.filter((t) => t.shouldDefer);
const warningTroubles = allTroubles.filter((t) => !t.shouldDefer);
// Different UI treatment for each

Leverage troublesByType

Use the indexed accessor for specific trouble types:

const enjoyByTroubles = holder.troublesByType["EnjoyByTrouble"] || [];
if (enjoyByTroubles.length > 0) {
  showWarning("Ingredient expiring soon");
}

Related Documentation