---
title: "Widget SDK"
description: "The GlassHome widget SDK API. defineWidget, Widget components, hooks, entity bindings, and theming."
canonical: https://glasshome.app/docs/widget-sdk
section: "SDK"
updated: 2026-06-27
---
# Widget SDK

`@glasshome/widget-sdk` (latest: **1.2.0**, published on npm) provides the `defineWidget` API, composable UI components, reactive hooks, the Home Assistant data layer, and theming utilities for SolidJS-based widgets.

The package also exports a runtime version constant, `SDK_VERSION`, which equals the installed package version. The host compares it against each widget's `manifest.sdkVersion` range.

This page is the **guide**: the important API in context, with examples. For the exhaustive list of every public export across the three entry points, see the [API Reference](/docs/widget-api-reference).

## On this page

- [`defineWidget`](#definewidget) and the [manifest fields](#manifest-fields)
- [`widgetFields`](#widgetfields) form helpers and a [complete widget example](#complete-widget-example)
- The [`<Widget>`](#widget-component) container, its [variants](#variants), and [slot subcomponents](#slot-subcomponents)
- The [`<WidgetDialog>`](#widgetdialog-component) settings dialog
- [Hooks](#hooks), the [Home Assistant data & service layer](#home-assistant-data--services), and the [`EntityView`](#the-entityview-shape) shape
- [Errors and empty states](#errors-and-empty-states), [theming](#theming), [entity utilities](#entity-utilities), and [build tooling](#build-tooling)

## defineWidget

The entry point for every widget. Combines a manifest (metadata), an optional Zod config schema, an optional `migrate` function, and a SolidJS component.

```ts
function defineWidget<C = Record<string, unknown>>(
  definition: WidgetDefinition<C>
): WidgetDefinition<C>
```

`WidgetDefinition<C>` shape:

```ts
interface WidgetDefinition<C = Record<string, unknown>> {
  manifest: WidgetManifest;
  configSchema?: ZodType<C, unknown>;
  migrate?: (config: Record<string, unknown>, fromConfigVersion: number) => Record<string, unknown>;
  component: (props: { config: C }) => any;
}
```

When you provide `configSchema`, `defineWidget` auto-populates `manifest.schema` (JSON Schema) and `manifest.defaultConfig` from it. TypeScript types are derived via `z.infer`. The dashboard renders the edit form from the schema automatically.

## Manifest fields

The `manifest` object inside `WidgetDefinition` is authoritative. The CLI reads `src/<name>/manifest.json` for build and publish metadata; `bun widget upgrade` writes `sdkVersion` back to that file. Both the inline `manifest` (in `defineWidget`) and `manifest.json` are used: the inline manifest is what the widget runtime loads; `manifest.json` is what the CLI reads. Keep them in sync (the build step warns on schema drift).

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Display name and identifier |
| `description` | `string` | no | Short description shown in the widget picker |
| `icon` | `string` | no | Iconify icon, e.g. `"mdi:lightbulb"` |
| `minSize` | `{w, h}` | yes | Minimum grid size |
| `maxSize` | `{w, h}` | yes | Maximum grid size |
| `defaultSize` | `{w, h}` | no | Initial size when added to a dashboard |
| `sdkVersion` | `string` | yes | Required SDK version range, e.g. `"^1.0.0"` |
| `capabilities` | `CapabilityGrant[]` | yes (1.x) | Home Assistant domains the widget reads or controls; approved by the user, enforced by the host. Empty array for widgets that touch no HA data. See [Capabilities & Permissions](/docs/widget-capabilities). |
| `configVersion` | `number` | no | Bump when config shape changes; used with `migrate` |
| `schema` | `object` | no | Auto-populated from `configSchema`. Do not set manually. |
| `defaultConfig` | `object` | no | Auto-populated from Zod `.default()` calls. |
| `cssUrl` | `string` | no | Set by the build when the widget emits a stylesheet. Do not set manually. |

## widgetFields

Pre-built Zod field helpers. Each attaches metadata that the dashboard uses to render the right form control.

| Helper | Renders as |
|---|---|
| `widgetFields.title()` | Text input labelled "Title", hint "Optional display name override" |
| `widgetFields.entityIds(domain)` | Multi-select entity picker filtered by domain |
| `widgetFields.singleEntity(domain)` | Single-select entity picker filtered by domain |
| `widgetFields.areaId()` | Area picker dropdown |

For other types, use Zod directly. `z.boolean()` renders a switch, `z.enum([...])` a dropdown, `z.number()` a number input. Add `.meta({ title: "Label" })` to set the display label.

## Complete widget example

A working example that mirrors how the official `@glasshome` widgets are written: a config schema, entities bound from config via `useEntities`, group aggregation via `useWidgetEntityGroup`, a tap-to-toggle gesture, and a settings dialog with schema-driven config editing.

The key wiring to notice: **you bind entities yourself** from the ids the user picked in config (`useEntities(() => props.config.entityIds)`), then pass that accessor into `useWidgetEntityGroup`. There is no hidden injection: the entity ids come from your config, and the SDK keeps the resolved entities reactive.

```tsx
// src/my-lights/index.tsx
import {
  defineWidget,
  Widget,
  WidgetDialog,
  useEntities,
  useService,
  useWidgetContext,
  useWidgetDialog,
  useWidgetEntityGroup,
  useWidgetGestures,
  widgetFields,
} from "@glasshome/widget-sdk";
// Dialog chrome (ResponsiveDialog, Button, SchemaForm) is injected, not bundled by the SDK.
// The official widgets re-export these from @glasshome/ui as a `widgetDialogProps` object.
import {
  Button,
  ResponsiveDialog,
  ResponsiveDialogContent,
  ResponsiveDialogDescription,
  ResponsiveDialogHeader,
  ResponsiveDialogTitle,
  SchemaForm,
} from "@glasshome/ui/solid";
import { onCleanup } from "solid-js";
import { z } from "zod";

const configSchema = z.object({
  title: widgetFields.title(),
  entityIds: widgetFields.entityIds("light"),
});

type Config = z.infer<typeof configSchema>;

function MyLightsWidget(props: { config: Config }) {
  const ctx = useWidgetContext();
  const { openDialog, setShowDialog, dialogProps } = useWidgetDialog();
  const { toggle } = useService();

  // Bind the entities the user picked in config. Reactive: updates when state changes.
  const entities = useEntities(() => props.config.entityIds);

  const { aggregatedData, emptyState, hasEntities } = useWidgetEntityGroup({
    entities,
    aggregationMode: () => "light",
    emptyStateConfig: {
      icon: <span>💡</span>,
      title: "No lights",
      message: "Hold to configure",
    },
  });

  const lightData = () => aggregatedData();
  const isOn = () => lightData()?.isOn ?? false;

  const gestures = useWidgetGestures(() => ({
    tap: () => toggle(entities().map((e) => e.id)),
    hold: { action: openDialog },
  }));
  onCleanup(gestures.dispose);

  return (
    <>
      <Widget
        gestures={gestures}
        variant="classic-glass"
        tone={isOn() ? "accent" : "neutral"}
        emptyState={emptyState()}
        loading={!hasEntities()}
      >
        <Widget.Title>{props.config.title || "Lights"}</Widget.Title>
        <Widget.Status>{isOn() ? "On" : "Off"}</Widget.Status>
        <Widget.Value>
          {lightData() ? `${lightData()!.onCount}/${lightData()!.totalCount}` : ""}
        </Widget.Value>
      </Widget>
      <WidgetDialog
        {...dialogProps}
        ResponsiveDialog={ResponsiveDialog}
        ResponsiveDialogContent={ResponsiveDialogContent}
        ResponsiveDialogHeader={ResponsiveDialogHeader}
        ResponsiveDialogTitle={ResponsiveDialogTitle}
        ResponsiveDialogDescription={ResponsiveDialogDescription}
        Button={Button}
        SchemaForm={SchemaForm}
        title="Lights"
        maxWidth="lg"
        configSchema={configSchema}
        config={props.config}
        onConfigSave={(config) => {
          ctx.updateConfig(config);
          setShowDialog(false);
        }}
      />
    </>
  );
}

export default defineWidget<Config>({
  manifest: {
    name: "My Lights",
    description: "Toggle a group of lights",
    icon: "mdi:lightbulb-group",
    minSize: { w: 1, h: 1 },
    maxSize: { w: 4, h: 4 },
    sdkVersion: "^1.0.0",
    capabilities: [{ domain: "light", access: "control" }],
  },
  configSchema,
  component: MyLightsWidget,
});
```

> **Binding entities from config:** `useEntities(() => props.config.entityIds)` resolves the ids the user picked (via the `widgetFields.entityIds` picker) into live `EntityView[]`. Pass that accessor straight into `useWidgetEntityGroup`. For a single-entity widget use `useEntity(() => props.config.entityId)` instead. The scaffold template ships this exact wiring.

> **The dialog needs UI components:** `<WidgetDialog>` renders no chrome of its own. It takes `ResponsiveDialog`, `Button`, `SchemaForm`, and friends as props so your widget controls the look. Import them from <code>@glasshome/ui/solid</code> (keep `@glasshome/ui` as a dependency). See [WidgetDialog component](#widgetdialog-component) for the full prop list.

## Widget component

`<Widget>` is the main container. It provides context, gesture handling, theming, and base styling.

### Props

```ts
interface WidgetProps {
  variant?: string | WidgetVariantConfig;
  tone?: Tone;           // semantic color: resolves to --widget-color
  color?: string;        // CSS color override for --widget-color (overrides tone)
  colorTo?: string;      // second gradient stop (--widget-color-to)
  gradient?: string;     // full CSS gradient (overrides auto-shell)
  loading?: boolean;
  class?: string;
  isEditMode?: boolean;
  onDelete?: () => void;
  emptyState?: { icon?: JSX.Element; title?: string; message?: string };
  gestures?: GestureHandlers; // return value of useWidgetGestures(...)
  children?: JSX.Element;
}
```

Pass the return value of `useWidgetGestures(...)` to `gestures`, not a plain object.

### Variants

`variant` picks a pre-built look (shell background, blur, layout defaults, hover/active behavior). Pass a built-in id (a string) or a full `WidgetVariantConfig` object. The SDK ships three:

| `variant` id | Look |
|---|---|
| `"classic-glass"` | Default glassmorphism: blurred translucent background, gradient shell, hover/active scale. What every official `@glasshome` widget uses. |
| `"minimal"` | Clean, no background effects. Tighter padding. |
| `"compact-horizontal"` | Horizontal layout for short, wide tiles. |

```tsx
<Widget variant="classic-glass" tone="accent">…</Widget>
```

> **No variant means no shell styling:** If you omit `variant`, no variant styling is applied (you get a bare shell). For the standard GlassHome look, pass `variant="classic-glass"`. The scaffold and every built-in widget do.

For a fully custom look, pass a `WidgetVariantConfig` object instead of a built-in id (that type is exported from the SDK). Tone and color props ([Theming](#theming), and [Styling](/docs/widget-styling#coloring-the-shell)) layer on top of whichever variant you choose.

### Slot subcomponents

| Slot | Key props | Purpose |
|---|---|---|
| `Widget.Icon` | `icon: JSX.Element`, `color?`, `dimmed?`, `entityCount?` | Entity icon with adaptive color |
| `Widget.Title` | `children` | Primary label |
| `Widget.Status` | `children` | State text (On, Off, etc.) |
| `Widget.Value` | `children` | Numeric or formatted value |
| `Widget.Content` | `children`, `class?` | Freeform content area |
| `Widget.SliderFill` | `value: number` (0–100), `color?`, `isDragging?` | Background fill for slider widgets |

`Widget.SliderFill` is also exported standalone as `WidgetSliderFill`; both render the same component. Pass `isDragging` so the fill tracks the finger 1:1 while dragging (it drops the transition). See [Animation](/docs/widget-styling#animation).

### Component props

Every widget component receives one prop: `{ config: C }` where `C` is the type you pass to `defineWidget<C>`. The host parses, migrates, and validates the saved config before passing it, so `config` always matches your current schema type.

## Hooks

### `useWidgetContext()`

Returns `ReactiveWidgetContext`. Must be called inside the `<Widget>` render tree.

```ts
interface ReactiveWidgetContext {
  isEditMode: () => boolean;
  updateConfig: (config: Record<string, unknown>) => void;
  dimensions: () => { width: number; height: number }; // CSS px, (0,0) before first layout
  registerDialogOpener?: (open: ((tab?: string) => void) | null) => void;
}
```

### `useWidgetEntityGroup(options)`

Aggregates one or many entities with built-in aggregation presets and empty-state support.

```ts
function useWidgetEntityGroup<TData = unknown>(
  options: UseWidgetEntityGroupOptions<TData>
): UseWidgetEntityGroupResult<TData>
```

**Options:**

| Field | Type | Description |
|---|---|---|
| `entities` | `Accessor<EntityView[]>` | Reactive entities array (required) |
| `emptyStateConfig` | `WidgetEmptyStateConfig` | Shown when `hasEntities` is false (required) |
| `aggregationMode` | `Accessor<AggregationPreset \| undefined>` | `"light"`, `"sensor"`, `"switch"`, `"binary-sensor"`, `"none"` |
| `sensorGroupType` | `Accessor<SensorGroupType>` | For `"sensor"` preset: `"min"`, `"max"`, `"mean"`, `"median"`, `"sum"`, `"last"`, etc. |
| `calculateGroupData` | `(entities: EntityView[]) => TData` | Custom aggregation function (alternative to `aggregationMode`) |
| `allEntitiesMode` | `boolean` | All entities must be on for group to be "on" (light/switch presets) |
| `minEntities` | `number` | Minimum entities required; default `1` |

**Return value:**

| Field | Type | Description |
|---|---|---|
| `entities` | `Accessor<EntityView[]>` | All entities |
| `groupData` | `Accessor<TData \| null>` | Result of `calculateGroupData`, or `null` |
| `aggregatedData` | `Accessor<LightGroupResult \| SensorGroupResult \| undefined>` | Result of preset aggregation |
| `emptyState` | `Accessor<WidgetEmptyStateConfig \| undefined>` | Defined when entities are missing |
| `hasEntities` | `Accessor<boolean>` | Whether minimum entities are present |
| `count` | `Accessor<number>` | Entity count |

`LightGroupResult` includes: `isOn`, `isUnavailable`, `brightness` (0-255), `brightnessPercent` (0-100), `color`, `onCount`, `totalCount`, `description`.

### `useWidgetDialog(defaultTab?)`

Controls the widget settings dialog. `defaultTab` defaults to `"controls"`.

```ts
interface WidgetDialogReturn {
  showDialog: () => boolean;
  setShowDialog: (open: boolean) => void;
  openDialog: (tab?: string) => void;
  closeDialog: () => void;
  activeTab: () => string;
  setActiveTab: (tab: string) => void;
  dialogProps: {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    activeTab: string;
    onActiveTabChange: (tab: string) => void;
  };
}
```

Spread `dialogProps` onto `<WidgetDialog>` to wire open/close and tab state automatically. The dialog itself is documented next.

## WidgetDialog component

`<WidgetDialog>` is the settings/detail dialog for a widget: a tabbed panel that can host device controls, a schema-driven config editor, and a debug view. It renders **no chrome of its own** so it can match your theme. You inject the dialog/button/form components as props (the official widgets import them from `@glasshome/ui/solid`). Spread `dialogProps` from `useWidgetDialog()` to wire its open/tab state.

### Injected UI components (required)

These have no defaults; the dialog will not render without them. Import from `@glasshome/ui/solid`:

| Prop | Component |
|---|---|
| `ResponsiveDialog` | Dialog/drawer wrapper |
| `ResponsiveDialogContent` | Body |
| `ResponsiveDialogHeader` | Header region |
| `ResponsiveDialogTitle` | Title |
| `ResponsiveDialogDescription` | Subtitle/description |
| `Button` | Buttons (save, delete, tab controls) |
| `SchemaForm` | Form renderer for schema-driven config editing. Required only when you use `configSchema`. |

> **Info:** The official `@glasshome` widgets bundle these into a single `widgetDialogProps` object and spread it (``). Do the same in your project to keep call sites short.

### Content props

| Prop | Type | Purpose |
|---|---|---|
| `title` | `string` | Dialog title (required) |
| `controlsContent` | `JSX.Element` | Device-controls tab (sliders, buttons for the bound entities) |
| `configSchema` | `ZodType` | Enables the schema-driven config editor tab. Pair with `config` + `onConfigSave`. |
| `config` | `Record<string, unknown>` | Current config to seed the editor |
| `onConfigSave` | `(config) => void` | Called with the new config; typically `ctx.updateConfig(config)` then close |
| `editContent` | `JSX.Element` | Custom config UI, used **instead of** the schema editor when `configSchema` is absent |
| `debugContent` | `JSX.Element` | Debug tab body |
| `debugData` | `string \| Record<string, unknown>` | Raw debug payload (rendered if no `debugContent`) |
| `tabs` | `WidgetDialogTab[]` | Fully custom tabs: `{ id, label, icon, content }[]` |
| `defaultTab` / `activeTab` + `onActiveTabChange` | `string` / controlled pair | Initial or controlled active tab |
| `headerActions` | `JSX.Element` | Extra buttons in the header |
| `onDelete` | `() => void` | Renders a delete action |
| `maxWidth` | `"sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "3xl" \| "4xl"` | Dialog max width |

A widget that only needs to edit config can pass `configSchema` + `config` + `onConfigSave` and nothing else; a control widget adds `controlsContent`. See the [complete example](#complete-widget-example) for the full wiring.

### `useWidgetGestures(config, orientation?)`

Tap, hold, and slide gestures on any pointer type.

```ts
function useWidgetGestures(
  config: () => GestureConfig,
  orientation?: () => "horizontal" | "vertical" | "square"
): GestureHandlers
```

Both arguments are SolidJS accessors. The orientation arg is optional (defaults to horizontal).

```ts
interface GestureConfig {
  tap?: () => void;
  hold?: { action: () => void; delay?: number }; // delay in ms, default 300
  slide?: {
    value: number;
    onChange: (value: number) => void;
    min?: number; max?: number;
    orientation?: "auto" | "horizontal" | "vertical";
    activationDelay?: number;
  };
}
```

`GestureHandlers` (returned value) carries `onPointerDown`, `onPointerMove`, `onPointerUp`, `onPointerCancel`, `onPointerEnter`, `bindElement`, `touchAction`, and `dispose`. Pass the whole object to ``. Call `dispose()` in `onCleanup`.

### `useReducedMotion()`

Returns `() => boolean`. Reactive `prefers-reduced-motion` accessor. Returns `false` when `matchMedia` is unavailable (SSR-safe).

### `useIntersectionPause(elAccessor)`

Returns `() => boolean`: `true` while the given element is off-screen. Pass an accessor returning the element to observe. Returns `false` when `IntersectionObserver` is unavailable (SSR-safe).

## Home Assistant data & services

All Home Assistant access goes through hooks re-exported from the SDK. Import them from `@glasshome/widget-sdk`, never from `@glasshome/sync-layer` directly (a direct import fails the build, and would bundle a second, disconnected store). Every read hook is a SolidJS accessor backed by the host's live store, so your UI stays reactive without managing any WebSocket state. Reads and service calls are checked against the [capabilities](/docs/widget-capabilities) your manifest declares.

### Reading entities

| Hook | Returns | Purpose |
|---|---|---|
| `useEntity(id)` | `Accessor<EntityView \| undefined>` | One entity by id (`id` may be a string or accessor) |
| `useEntities(ids)` | `Accessor<EntityView[]>` | Many entities; pass an accessor returning the id array |
| `useArea(id)` | `Accessor<AreaView \| undefined>` | An area and its entities |
| `useEntityHistory(id)` | `Accessor<EntityHistoryData \| undefined>` | Recent state history for an entity |
| `useEntityStatistics(id, options)` | `Resource<StatisticValue[]>` | Long-term statistics; the resource exposes `.loading` and `.error` |
| `useForecast(id)` | `Accessor<WeatherForecastsData \| undefined>` | Weather forecast for a weather entity |
| `useCamera(id)` | `{ stream, refresh }` | Reactive camera stream data |
| `useStore(selector)` | `Accessor<T>` | Escape hatch for direct store access when no specific hook fits |

```tsx
import { useEntity, useEntities } from "@glasshome/widget-sdk";

function Thermostat(props: { config: Config }) {
  const climate = useEntity(() => props.config.entityId);
  const temp = () => climate()?.attributes.current_temperature;
  return <Widget.Value>{temp() ?? "—"}</Widget.Value>;
}
```

### The `EntityView` shape

Every read hook returns one or more `EntityView` objects. The fields you reach for most:

| Field | Type | Notes |
|---|---|---|
| `id` | `string` | Entity id, e.g. `"light.kitchen"` |
| `domain` | `string` | `"light"`, `"sensor"`, … |
| `state` | `string` | Current state value (`"on"`, `"22.5"`, …) |
| `attributes` | `Record<string, any>` | HA attributes, **minus** the four surfaced below. Read those from the canonical fields, not from here. |
| `friendlyName` | `string` | Display name |
| `icon` | `string \| null` | Resolved icon (registry → attribute → domain default) |
| `deviceClass` | `string \| null` | Resolved device class |
| `unitOfMeasurement` | `string \| null` | Resolved unit |
| `areaId` / `deviceId` | `string \| null` | Placement / device |
| `lastChanged` / `lastUpdated` | `Date` | Timestamps |

`deviceClass`, `unitOfMeasurement`, `friendlyName`, and `icon` are lifted out of `attributes` into canonical fields, so always read them there. `EntityView` is exported as a type from the SDK for your own annotations.

### Connection & locale

| Hook | Returns |
|---|---|
| `useConnection()` | `{ status, isConnected }` accessors |
| `useHassConfig()` | `Accessor<HassConfig \| null>` |
| `useUnitSystem()` | `Accessor<HassUnitSystem \| null>` |
| `useTemperatureUnit()` | `Accessor<string>` (e.g. `"°C"`) |
| `useLocale()` | `Accessor<string>` (BCP 47) |
| `useCurrency()` | `Accessor<string>` (ISO 4217) |

### Calling services

`useService()` returns a capability-routed service caller plus the common shortcuts. The shortcuts take an entity id (or array) and optional service data:

```tsx
import { useService } from "@glasshome/widget-sdk";

function LightToggle(props: { config: Config }) {
  const { toggle, turnOn, callService } = useService();

  return (
    <Widget gestures={useWidgetGestures(() => ({ tap: () => toggle(props.config.entityId) }))}>
      {/* turnOn("light.x", { brightness_pct: 80 }) */}
      {/* callService("light", "turn_on", { brightness_pct: 80 }, { entity_id: "light.x" }) */}
    </Widget>
  );
}
```

| Hook | Returns |
|---|---|
| `useService()` | `{ callService, turnOn, turnOff, toggle }` |
| `useTurnOn()` / `useTurnOff()` / `useToggle()` | The single shortcut function |

`callService(domain, service, data?, target?)` is the general form; it resolves to `Promise<void>`. The host validates the call against your granted capabilities before forwarding it to Home Assistant.

## Errors and empty states

- **Config schema parse failure.** If the stored config fails Zod validation after migration, the dashboard falls back to `configSchema.parse({})` (all-defaults). If that also fails, it passes `{}` to the component. No error is shown to the user; your component should handle missing/default config gracefully.
- **Missing entities.** Pass an `emptyStateConfig` to `useWidgetEntityGroup`. When `hasEntities()` is false, `emptyState()` returns the config object; pass it to `<Widget emptyState={emptyState()}>` to render the built-in empty state UI.
- **Component throw.** If your component throws during render, the dashboard catches it with an `ErrorBoundary`. After one crash it shows a retryable error card with a Retry button. After three crashes (configurable in the Dash host) it shows a permanent "disabled" card until the page is refreshed.

## Theming

Widgets inherit the dashboard theme automatically and render in an isolated shadow root with their own CSS bundle. The essentials:

- Use the `tone` prop on `<Widget>` for semantic color (`tone="accent"`), or `color`/`colorTo`/`gradient` for a custom shell.
- `isDark()` returns the current theme as a boolean for logic; `dark:` Tailwind variants work in markup.
- The SDK also exports `injectTokens`, `Tone`, and `ToneSchema`.

The full styling model (Tailwind, your own CSS files, theme variables, container queries, and animation) is covered in **[Styling & Animation](/docs/widget-styling)**.

## Entity utilities

| Function | Description |
|---|---|
| `isEntityActive(entity)` | `true` for active states |
| `getEntityAttribute(entity, key)` | Read a specific attribute |
| `countActiveEntities(entities)` | Count of currently active entities |
| `calculateLightGroup(entities, allEntitiesMode?)` | Aggregate brightness/color/state across a light group |
| `calculateSensorGroup(entities, groupType?, ignoreNonNumeric?)` | Aggregate numeric sensors |

## Build tooling

The scaffolded `vite.config.ts` wires this up automatically. Two entrypoints for custom setups:

### `@glasshome/widget-sdk/vite`

| Export | Purpose |
|---|---|
| `glasshomeWidget(options?)` | Single-widget plugin: dev preview + library build |
| `glasshomeWidgets(options?)` | Multi-widget project plugin: per-widget builds + `registry.json` |
| `buildWidgets(options?)` | Programmatic build of all widgets (used by `bun widget build`) |
| `discoverWidgets(srcDir)` | Scan `src/` for subdirs containing `index.tsx` + `manifest.json` |
| `generateRegistry(srcDir, outDir)` | Write a `registry.json` from discovered manifests |
| `isWidgetExternal(id)` | `true` for host-provided packages (not bundled) |

### `@glasshome/widget-sdk/schemas`

Zod schemas for validating manifests and publish payloads. Used by the CLI and Hub.

These are re-exported from the canonical [`@glasshome/widget-contract`](/docs/widget-capabilities) package (the single source of truth shared by the SDK, the CLI, the Hub, and Dash), so the same schema validates a manifest everywhere.

| Export | Purpose |
|---|---|
| `widgetManifestSchema` (alias `WidgetManifestSchema`) | Validate a complete widget manifest |
| `publishManifestSchema` | Validate the manifest subset sent at publish time |
| `capabilitiesSchema` | Validate a `capabilities` array |
| `capabilityGrantSchema` | Validate a single `{ domain, access }` grant |
| `GridSizeSchema` | Validate `{ w, h }` |
| `parseGridSize` / `serializeGridSize` | Parse from / serialize to the stored JSON form |
| `formatSchemaError` | Turn a Zod error into a readable message |
| `PublishRequestSchema` / `PublishConfirmSchema` / `PublishBodySchema` | Publish-flow request bodies |
| `CapabilityGrant` (type) | TypeScript type for one grant |

`formatSchemaError` and `WidgetManifestSchema` are also available from the SDK main entry (`@glasshome/widget-sdk`) for convenience.