This guide explains the consumer-facing model hierarchy provided by the dispense SDK for building beverage selection interfaces. These models represent the dispenser from a consumer’s perspective, focusing on product selection and pouring rather than hardware details.
The consumer UI hierarchy (DeviceAssembly → Nozzle → Availability) is distinct from the hardware hierarchy (Assembly → Holder/Pump) and optimized for consumer-facing applications where users select brands, beverages, and flavors.
Why a Separate Hierarchy?
Consumer UIs need selection-focused models, not hardware-focused models:
- Hardware hierarchy: Targets crew/maintenance apps needing holder, pump, and board access for ingredient management, calibration, and troubleshooting
- Consumer hierarchy: Targets consumer apps needing brand, beverage, and flavor selection for intuitive product discovery and pouring
The consumer hierarchy provides:
- Product-centric navigation (brands → beverages → flavors)
- Availability and visibility management
- Selection state tracking
- Pour operations without hardware complexity
- Real-time availability updates via WebSocket
- Companion model support for specialized behavior
Model Overview
The SDK provides three core consumer UI models:
- DeviceAssemblyModel: Entry point representing device from consumer perspective
- NozzleModel: Dispense point with selection capabilities and pour operations
- AvailabilityModel: Selectable item (brand, beverage, flavor, add-on)
DeviceAssemblyModel
Singleton entry point for consumer applications. Loads nozzles from device topology and provides access to dispense points.
Key Properties
interface DeviceAssemblyModel {
// Identity
id: string;
// Nozzle Access
nozzlePaths: string[]; // All nozzle paths on device
}Usage
DeviceAssemblyModel is a singleton—one instance per device:
import { DeviceAssembly } from "@kosdev-code/kos-dispense-sdk";
import { getKosModel } from "@kosdev-code/kos-ui-sdk";
// Get singleton instance
const { model: deviceAssembly } = await getKosModel(DeviceAssembly.type);
// List all nozzles
console.log("Nozzle paths:", deviceAssembly.nozzlePaths);
// Get specific nozzle
const nozzle = deviceAssembly.getNozzle("assembly:core:nozzle:nozzle");
if (nozzle) {
console.log("Nozzle available:", nozzle.name);
}Lifecycle
During the load phase, DeviceAssemblyModel:
- Calls
getAssembly()service to fetch device topology - Extracts nozzle data from assemblies
- Creates NozzleModel instances for each nozzle
- Stores nozzles in internal container
This is the same topology data used by AssemblyModel, but DeviceAssemblyModel only extracts nozzle information needed for consumer UIs.
AvailabilityModel
Represents a selectable item in the availability graph. Can represent brands, beverages, flavors, add-ons, or any selectable product.
Key Properties
interface AvailabilityModel<T = any, P = any> {
// Identity
id: string; // Unique model identifier
rawId: string; // Original ID from backend
altId: string; // Alternative ID for correlation
type: string; // Type classification
// Organization
groupId: string; // Group membership (brands, beverages, flavors)
parentId?: string; // Parent relationship for drill-down
taggedIds: string[]; // Array of related IDs
// State
available: boolean; // Whether item can be poured
visible: boolean; // Whether item should display in UI
note?: string; // Optional note
// Extension Data
data: T; // Custom typed data
properties: P; // Additional typed properties
}Usage
Availability models are typically accessed through NozzleModel:
// Access via group
const brands = nozzle.brands;
brands.forEach(brand => {
console.log("Brand:", brand.data.name);
console.log("Available:", brand.available);
console.log("Visible:", brand.visible);
});
// Check state
if (brand.available && brand.visible) {
// Show in UI and allow selection
}
// Access custom data
interface BrandData {
name: string;
imageUrl: string;
brandColor: string;
}
const brand: AvailabilityModel<BrandData> = ...;
console.log(brand.data.name);
console.log(brand.data.imageUrl);
console.log(brand.data.brandColor);
// Access properties
interface BeverageProperties {
calories: number;
sugar: number;
}
const beverage: AvailabilityModel<any, BeverageProperties> = ...;
console.log(beverage.properties.calories);
console.log(beverage.properties.sugar);Parent-Child Relationships
Availability models support hierarchical navigation via ParentAware:
// Find child items
const beverages = nozzle.getAvailabilityByParent(brand.id);
beverages.forEach(beverage => {
console.log("Beverage under brand:", beverage.data.name);
});
// Tagged relationships
brand.taggedIds.forEach(taggedId => {
const beverage = nozzle.beverages.find(b => b.rawId === taggedId);
if (beverage) {
console.log("Related beverage:", beverage.data.name);
}
});Relationship types:
- parentId: Direct parent reference (set by
taggedIdsprocessing) - taggedIds: Array of related item IDs (from backend)
State Management
Availability models support state updates:
// Update availability
availability.updateAvailability(false); // Mark unavailable
availability.updateAvailability(true); // Mark available
// Check state changes (reactive via kosAutoEffect)
kosAutoEffect(() => {
if (!availability.available) {
console.log("Item became unavailable");
}
});State properties:
available: Whether item can be poured (determined by pour engine)visible: Whether item should display in UI (determined by pour engine)- Both properties update automatically via WebSocket
Extension Data
Availability models support typed custom data:
// Define custom data type
interface BeverageData {
name: string;
description: string;
imageUrl: string;
nutritionalInfo: {
calories: number;
sugar: number;
caffeine: number;
};
}
// Access typed data
const beverage: AvailabilityModel<BeverageData> = ...;
console.log(beverage.data.name);
console.log(beverage.data.description);
console.log(beverage.data.nutritionalInfo.calories);
// Properties for additional metadata
interface BeverageProperties {
popular: boolean;
seasonal: boolean;
allergens: string[];
}
const beverage: AvailabilityModel<BeverageData, BeverageProperties> = ...;
console.log(beverage.properties.popular);
console.log(beverage.properties.seasonal);Extension data populated via ExtensionType.AvailabilityMapper during nozzle load. See Extension Points for details.
Companion Models for Specific Behavior
While ExtensionType.AvailabilityMapper adds custom data, companion models add specific behavior and methods. This allows generic AvailabilityModel to be specialized (Brand, Beverage, Flavor) with type-specific operations.
When to use companions vs extension data:
- Extension data (
AvailabilityMapper): Additional properties (icons, names, colors, metadata) - Companion models: Type-specific behavior (child beverage lists, custom methods, app-specific operations)
Example: BrandCompanion for brand-specific behavior
// Companion provides brand-specific behavior
interface BrandCompanion extends KosCompanionDecorator<AvailabilityModel> {}
@kosModel("brand-companion")
@kosCompanion() // Decorator mode - transparent proxy to parent
class BrandCompanion implements IKosDataModel {
id: string;
// Brand-specific method
getChildBeverages(nozzle: NozzleModel): AvailabilityModel[] {
return nozzle.getAvailabilityByParent(this.id); // 'id' from parent via proxy
}
// Brand-specific computed property
get brandColor(): string {
return this.data.brandColor; // 'data' from parent via proxy
}
// Brand-specific operation
async loadBrandAssets(): Promise<void> {
// Load brand-specific resources using this.rawId, this.data, etc
}
}
// Access companion
const brand: AvailabilityModel = ...;
const brandCompanion = getKosCompanionModel<BrandCompanion>(
brand,
BrandCompanion.type
);
const childBeverages = brandCompanion.getChildBeverages(nozzle);
// Companion acts as transparent proxy - all parent properties available
console.log(brandCompanion.name); // From AvailabilityModel.data.name
console.log(brandCompanion.available); // From AvailabilityModelBenefits of companion pattern:
- Extends SDK models without modifying source code
- Type-safe access to specialized behavior
- Clean separation of generic framework vs app-specific logic
- Automatic lifecycle management (created/destroyed with parent)
- Transparent proxy mode makes companion drop-in replacement for parent
See Extension Points for complete companion model documentation.
Use Cases
Availability models represent any selectable item:
- Brands: Top-level product categories
- Beverages: Individual drink products
- Flavors: Flavor additions/shots
- Add-ons: Customizations or enhancements
- Sizes: Volume options (if modeled as items)
All use the same AvailabilityModel interface with different groupId values.
Complete Example
Basic consumer selection workflow:
import { DeviceAssembly } from "@kosdev-code/kos-dispense-sdk";
import { getKosModel } from "@kosdev-code/kos-ui-sdk";
// 1. Get device and nozzle
const { model: deviceAssembly } = await getKosModel(DeviceAssembly.type);
const nozzle = deviceAssembly.getNozzle("assembly:core:nozzle:nozzle");
if (!nozzle) {
throw new Error("Nozzle not found");
}
// 2. Display brands
console.log("Available brands:");
nozzle.visibleBrands.forEach(brand => {
console.log(`- ${brand.data.name}`);
});
// 3. User selects brand
const selectedBrand = nozzle.visibleBrands[0];
nozzle.setSelectedBrand(selectedBrand);
// 4. Display beverages for brand
const beverages = nozzle.getAvailabilityByParent(selectedBrand.id);
console.log(`Beverages for ${selectedBrand.data.name}:`);
beverages.forEach(beverage => {
console.log(`- ${beverage.data.name} (${beverage.available ? "available" : "unavailable"})`);
});
// 5. User selects beverage
const selectedBeverage = beverages.find(b => b.available);
if (!selectedBeverage) {
throw new Error("No available beverages");
}
await nozzle.setSelectedPourable({
beverage: selectedBeverage
});
// 6. Execute pour
if (nozzle.canPour) {
console.log("Starting pour...");
await nozzle.pour();
console.log("Pour complete!");
} else {
console.log("Cannot pour - beverage unavailable or already pouring");
}For complete consumer UI patterns including flavor selection and pour tracking, see Consumer UI Kit (CUI Kit).
Next Steps
- React Integration: Use pre-built contexts and hooks for React applications
- Consumer UI Kit (CUI Kit): Complete brand → beverage → flavor selection workflow
- Pouring Operations: Deep dive into pour execution and tracking
- Extension Points: Customize availability data, selection payloads, and add companion models
