UI Controls
UI Controls are navigation buttons and pages that appear in the main navigation of each UI. They determine which control pages (Lighting, AV, HVAC, etc.) are accessible from each user interface.
Overview
UI Controls allow you to:
- Customize which control pages appear in each UI
- Create UI-specific controls (hide irrelevant controls)
- Configure control icons and labels
- Reorder navigation buttons
- Set PIN protection for specific controls
Viewing UI Controls
The page has two tabs: List and Select.
List Tab
The main grid displays all UI controls with the following columns:
- ID - Unique identifier
- Label - Display text on navigation button
- Component - Svelte component path
- UI - UI this control belongs to (null = global)
- Icon - Icon filename
- PIN - Whether PIN is required to access
- Enabled - Whether the control is active
- Sort Index - Display order
Grid Actions
- Add - Create a new UI control
- Delete - Remove a UI control
- Sort - Reorder controls by dragging rows
Quick Actions
- Duplicate Icon - Copy an existing control
Understanding Global vs. UI-Specific Controls
Global Controls (UI = null)
- Appear in ALL UIs by default
- Cannot be removed from a specific UI
- Include standard controls: Lighting, Audio, Video, Climate
UI-Specific Controls (UI = specific ID)
- Appear only in the assigned UI
- Override global controls when present
- Allow per-UI customization
Hiding Global Controls
To hide a global control from a specific UI:
- Click Duplicate icon on the global control
- Set UI to the target UI ID
- Set Enabled = No
- Save
Result: The disabled UI-specific control overrides the global control, effectively hiding it from that UI.
Tip: A tooltip on the List tab explains this: "To hide a global control, copy it, add it to a specific UI, then disable it"
Space-Linked UIs
When a UI has site_space_id set, its control list is auto-derived from the subsystems present in the linked Site Space (and descendants if include_child_spaces is on). Explicit ui_control rows are still honored as overlays:
- A row with a new
componentadds a control on top of the inherited set. - A row with the same
componentas an inherited control andenabled = falsesuppresses that inherited control. - An enabled row with the same
componentas an inherited control does not appear twice — but its PIN is carried onto the inherited tile. This is how you PIN-protect an auto-derived control: add aui_controlrow for that component with a PIN set. If both a per-UI row and an All-UIs global carry a PIN, the per-UI PIN wins. - Global ("All UIs") controls overlay space-linked UIs too. A control assigned to All UIs now appears on space-linked UIs alongside the auto-derived set, matching how non-space UIs union global controls. A per-UI disabled row for the same
componentstill suppresses it.
The Select tab shows a banner reminding you of this when a space-linked UI is selected.
Because a space-linked UI's controls are derived from the zoned subsystems in its space, non-zoned pages like Weather would never appear automatically. When one or more weather locations are configured in the system settings, a /controls/weather control is auto-injected into space-linked UIs (sorted toward the end of the menu). Add a disabled /controls/weather overlay row to suppress it, or it falls away on its own once all weather locations are removed.
Select Tab
The Select tab provides a quick way to add standard controls to a UI:
Workflow
Left Side - Available Controls:
- Lists all subsystem-based controls
- Includes: Lighting, Shades, Security, HVAC, etc.
- Plus special controls: Audio, Video
Center - Action Buttons:
- Add ▶ - Add selected controls to UI
- ◀ Remove - Remove selected controls from UI
Right Side - Current UI Controls:
- Shows controls already assigned to selected UI
- Sorted by sort_index
- Edit Selected (or double-click a row) opens the full control editor in a modal over this tab
Using the Select Tab
- Select a UI from the dropdown at the top
- Choose controls from the left list (multi-select)
- Click Add ▶ to add them to the UI
- Or select existing controls on the right and click ◀ Remove
Editing an assigned control in place
The assigned list is where a wrong label or icon is usually noticed. Select exactly one assigned control and click Edit Selected — or double-click it — to open the same editor the List tab uses, in a modal. Closing the modal refreshes both the assigned list and the List tab grid, so a renamed control shows its new label immediately.
Editing more than one at a time is not supported; the button reports "Select one assigned UI control to edit" if the selection is empty or has several rows.
Component Path: Controls are automatically mapped to /controls/{subsystem_lowercase}
Example:
Lighting → /controls/lighting
Audio → /controls/audio
Shades → /controls/shades
Control Configuration
Basic Properties
Label
- Text shown on navigation button
- Examples: "Lighting", "Audio", "Climate", "Cameras"
- Can be customized per UI
Component
- Svelte component path
- Must start with
/ - Standard format:
/controls/{name} - Custom components:
/controls/custom_control
UI
- Leave blank for global control (all UIs)
- Select specific UI for UI-only control
- Use for UI customization
Icon
- Icon filename (without extension)
- Resolved theme-first: the theme's
/icons/directory is checked, then GEM's built-in icon set - Examples:
lighting,audio,shades,security - If not set, defaults based on the subsystem in the component name. Subsystem
names are plural while most glyph files are singular, so the mapping is
explicit (
fans→fan,doors→door,power→outlet,lifts→lift,locks→lock). A subsystem with no mapping — anything you created yourself — falls back to the genericcontrolsglyph, so set an icon explicitly to give it its own. The same mapping drives the nav rail / tab bar items and the zone pins on the floor-plan map, so all three agree
PIN
- Require PIN entry before accessing control
- Useful for:
- Parental controls
- Secure controls (security system)
- Administrative functions
Enabled
- Toggle to show/hide control
- Disabled controls don't appear in navigation
- Configuration preserved
Sort Index
- Display order in navigation
- Lower numbers appear first
- Drag rows in the grid, then click the Save Sort Order button (swap-sort icon) in the toolbar to renumber
Common Use Cases
Adding Standard Controls to a UI
Using Select tab:
- Select UI:
kitchen_panel - Select controls: Lighting, Shades, Climate
- Click Add ▶
- Controls appear in kitchen panel navigation
Creating Custom Control
- Switch to List tab
- Click Add
- Configure:
Label: SecurityComponent: /controls/securityUI: master_bedroom_panelIcon: shieldPIN: Yes (require PIN)Enabled: Yes
- Save
Hiding Global Control from Specific UI
To hide "Video" from guest panel:
- Find "Video" global control in list
- Click Duplicate icon
- In the copy, set:
UI: guest_panelEnabled: No
- Save
- Video control no longer appears in guest_panel
Reordering Controls
Using List tab:
- Enable Sort action
- Drag controls to desired order
- Sort index updates automatically
- Changes take effect immediately in UIs
Or using Select tab (right side):
- Select a control from the right list
- Use ▲ ▼ buttons to move up/down
- Sort index updates automatically
User Reordering (press and hold)
End users can reorder the controls grid directly on their UI: press and
hold a tile until it lifts, drag it to a new position, and release. The
order is saved per UI as the control_order attribute and overlays the
integrator-defined sort — it also drives the nav rail / bottom tab bar
shortcuts (nav_layout: rail), so the first few controls in the grid are
the ones that appear as shortcuts.
- Controls the integrator adds later append after the user-ordered ones, in their sort index position.
- Deleting the UI's
control_orderattribute (Attributes tab) restores the sort-index ordering. - The Lock Control Order switch on the UI editor's Display tab
(
lock_control_ordersetting) disables the gesture for that UI — set it on the shared defaults (ui 0) to lock every UI, and override per UI where reordering should stay available. The lock is enforced server-side.
Tile Status Headlines
Each tile on the controls grid shows a live status line beneath its label, and the tile takes the accent color while something is on/playing/open:
- Audio / Video — playing zone count (
2 zones playing) or Off - Lighting / Fans / Power — zones on (
3 on) or Off - Shades — zones open (
2 open) or Closed - Garage / Gates — doors open (
1 open) or Closed - Doors —
2 unlocked, or Locked once every door with a known state is secured - Security — the panel's own arm state: Armed · Away, Armed · Home, Armed · Night, Exit Delay, Entry Delay, Alarm, or Disarmed
- Climate — current temperature (
72°) - Cameras — camera count
- Custom subsystems — any subsystem you created yourself (a sauna, pool equipment, a brewery) gets a generic headline automatically:
N activewhenever zones are in an active state. A zone counts as active when it reports a level above zero or any state that isn't a common at-rest word (off,closed,locked,disarmed,idle,standby,ok,normal,clear,secure,dry,offline,disabled, …) — unrecognized vocabularies likeheatingorrunningread as active, so custom subsystems light up with no configuration. The weather tile shows no headline
Headlines update in real time from the server's per-space status rollup where available, falling back to a zone snapshot taken when the page opens.
The Security tile is the exception: its headline comes from the arm_state attribute of the device attached to the security subsystem — the alarm panel itself — and outranks both zone paths. A panel that publishes its partitions as zones was already readable that way, but one that publishes sensor contacts as zones (most of them) has no zone that knows the arm mode, so a zone-derived reading called an armed system Disarmed. Reading the panel is also what puts Exit Delay, Entry Delay and Alarm on the tile, and the tile takes the accent color for all of those as well as for any armed mode. The value is live — arming from a keypad moves the tile without a page reload. If the Security subsystem has no device, or the panel has never reported an arm state, the tile shows no headline rather than guessing, and an arm state from a panel driver using its own vocabulary is shown as-is.
PIN-Protected Controls
For secure access to certain controls:
- Edit the control (e.g., "Security")
- Set PIN to the code that should unlock it
- Save
- When users click the control:
- PIN keypad appears
- Must enter the control's PIN
- On success, control opens
- On failure, the keypad shakes and clears
The typed code is checked on the server. The PIN itself is never sent to the browser — a non-elevated session reads only a derived pin_required flag on the control row, so the code cannot be recovered from a wall panel or guest tablet; the admin editor (an elevated read) still round-trips the value normally. Resolution matches control lookup: the panel's own UI-specific row is checked first, then the global All UIs row. A connection that submits ten wrong codes stops being answered until the client reconnects — a script cannot walk the 4-digit space, while a resident who fat-fingers the keypad is unaffected because a correct code resets the count.
Control Components
Standard Control Components
GEM includes built-in control components:
/controls/lighting
- Zone grid for lighting control
- Supports on/off, dimming, color
- Scene buttons if configured
- The left pane switches between Keypad, Scenes, and Groups via a segmented control in the bottom bar; individual lights stay in the right pane, and the right pane is titled with the selected group's name (Study) rather than a generic heading
- Each button in the Groups list badges how many of that group's lights are on (
3 on), live, so which rooms have something burning reads without opening them. A group with nothing on carries no badge. The All group counts each light once even when it belongs to several groups. The count uses the same judgement as View Active below - The All group is always the first button in the list. It is a view over every other group rather than a peer of them, so it stays pinned at the top whether the UI sorts groups by sort index or by name (
ui_group_sort, see UIs → Sorting); the remaining groups sort normally beneath it - The Keypad segment appears automatically when the UI carries
lutron_control_station_idattributes (stamped by thelutron_qsxdriver'ssync_areascommand — zero configuration) and renders the area's physical Lutron keypads on-screen with live LED state, one per station, on a shared wallplate like the installed hardware. Each keypad is titled with its Lutron area name, so a UI covering several stations reads apart at a glance. It becomes the default left-pane view when present, so clients land on the wall controls they already know — station discovery reads the UI's settings directly, so the keypad view paints immediately without flashing the scene list first. If the Lutron processor device is removed, the segment hides rather than showing errors - In the All group view, a light whose label doesn't already start with its zone group's name leads with the group label, so same-named lights in different rooms read apart; individual group views keep the original labels
- A View Active button in the bottom bar trims the list to lights that are currently on, judged from each zone's tracked state/level — a light hides when its state is
off, its level is 0, or it reports no state at all; every control type is judged the same way. The same button toggles back to View All and highlights while the filter is engaged. The filter is suspended while editing a scene, which needs the full list. When the filter is on and no lights qualify, the list shows No active zones instead of an empty column - A Map button appears when the UI's site space resolves to a floor plan, and opens the lighting zone map. Tap pins to select lights, then Off / Med / High to set every selected light at once; the X in the plan header closes the map. Each pin paints its light's live on/off state from the moment the plan opens, so the plan agrees with the list without waiting for the next change, and its halo blooms with the light's brightness — a lamp at 10% shows a tight ring, one at full a wide glow. Pins print the zone name under the disc wherever the plan has room, and touching and holding any pin names it in a callout; the current selection is echoed over the foot of the plan (
Kitchen Island +2) so you can confirm what Off is about to reach. The plan header names the subsystem after the floor — First Floor Lighting, taking the wording from the lighting subsystem's own label — since the same plan serves every control page, and the overlay covers the whole screen including the app's header bar. In portrait only one of the bottom bar's two button rows is on screen at a time, so the button appears in both rows; landscape shows both rows and carries it once, on the right. Pin positions are derived from site space geometry — see Site Spaces → End-user floor plan maps and Knowing which zone a pin is - A room on the plan whose space owns a UI carrying
lutron_control_station_idattributes shows a small keypad glyph beside its name; tapping it opens that room's real keypad in a modal, the same wallplate the Keypad segment renders. Nothing is configured — the glyph follows the linkssync_areasalready made. The Shades and generic zone-grid maps offer the same affordance. See Site Spaces → Reaching a room's keypad from the plan
/controls/audio
- Audio zone control
- Volume, input selection, transport
- Multi-room audio grouping
- A Map button in the bottom bar of both views opens a floor plan of the AV rooms, whenever the UI's site space resolves to one. AV rooms are ordinary zones in the
avsubsystem, so their pins are derived from site space geometry like every other map. In By Room the plan is a way of finding a room — tapping a pin selects it and opens its sources, exactly as tapping its tile does — and the button is hidden on a single-room site. In By Source the plan is a target picker: with a source chosen, tap the rooms to send it to and press Play Here, or Off to stop them. A lit pin means the room is on —source > 0on the zone — in both views, so a room already playing a different source reads as occupied rather than free; its halo blooms with the room's volume. The plan header names the page after the floor — First Floor Audio — since the same plan serves the video rooms and every other control page. See AV Zones → Floor plan map
/controls/video
- Video zone control
- Source selection, display control
- AV matrix routing
- The same Map button as the Audio page, pinning only the video rooms — a Video page never shows the audio zones even though both live in the
avsubsystem. See AV Zones → Floor plan map
/controls/shades
- Shade position control
- Open, close, stop, presets
- Group control — the right pane is titled with the selected group's name (Study) rather than a generic heading
- Each button in the Groups list badges how many of that group's shades are open (
2 open), live. A group with nothing open carries no badge. The All group counts each shade once even when it belongs to several groups. The count uses the same judgement as View Active below, so mid-travel shades count as open - The All group is always the first button in the list, under either group sort order — see the Lighting page above
- In the All group view, a shade whose label doesn't already start with its zone group's name leads with the group label (e.g.
Master Sheervs.Study Sheer); individual group views keep the original labels - A View Active button in the bottom bar trims the list to shades that are open, judged from each zone's tracked state/level — a shade hides when its state is
closed/off, its level is 0, or it reports no state at all; mid-travel states (opening, closing, stopped) count as open, and every control type is judged the same way. The same button toggles back to View All and highlights while the filter is engaged. When the filter is on and no shades qualify, the list shows No active zones instead of an empty column - A Map button appears when the UI's site space resolves to a floor plan, and opens the shade zone map. Tap pins to select shades, then Open / Close / Stop to drive every selected shade at once; the X in the plan header closes the map. Each pin paints its shade's live open/closed state from the moment the plan opens, and its halo blooms with how far open the shade is. Pins print the zone name under the disc where the plan has room, touch-and-hold names any pin, and the selection is echoed over the foot of the plan (
Living Sheer +3) — including shades hidden inside a count marker or sitting on another floor. The plan header names the subsystem after the floor — First Floor Shades, taking the wording from the shade subsystem's own label — and the overlay covers the whole screen including the app's header bar. In portrait only one of the bottom bar's two button rows is on screen at a time, so the button appears in both rows; landscape shows both rows and carries it once, on the right. See Site Spaces → End-user floor plan maps
/controls/climate
- Thermostat control
- Temperature, mode, fan
- Multi-zone HVAC
/controls/security
- Alarm panel interface
- Arm, disarm, status
- Zone bypass
/controls/cameras
- Camera feeds
- PTZ control
- Recording playback
- When viewing a group, tiles auto-arrange to fit the screen — the grid is sized to the number of cameras (1, 2×2, 3×3, etc.) and reflows for portrait vs. landscape orientation. Groups of 13 or more fall back to a generalized square-ish grid (roughly
√ncolumns) so every tile stays visible; portrait keeps two columns and stacks the rest - Tapping a tile expands that camera to full screen; tapping again returns to the group. Every tile is rebuilt on each of those transitions, but the connections behind them are held open and their streams are handed straight to the new tiles, so expanding and collapsing does not restart anything (see ONVIF → What a video tile shows before its first frame)
- A camera carrying audio (
stream_audio,videomode only) shows a speaker button in the bottom-left of its tile. Tiles arrive muted, so a grid stays silent until someone presses one — that press is also the browser gesture that lets audio play at all. Unmuting survives the stream being rebuilt, but a tile that is itself torn down and re-created — expanding a camera to full screen, or returning to the page — starts muted again (see ONVIF → Audio) - A Map button appears when the UI's site space resolves to a floor plan, and places each camera on the plan; tapping a pin opens that camera. The X in the plan header returns to the grid. Pins print the camera name under the disc where the plan has room; where it doesn't, touch and hold the pin to see the name before lifting — the tap only commits if your finger lifts on the same pin, so sliding off backs out without opening the wrong camera. Every pin carries the same identity colour rather than a state colour — a camera has no on/off to report, so nothing about it changes what its pin says (see Site Spaces → Camera pins take one identity colour). See Site Spaces → End-user floor plan maps
/controls/garage
- Garage door control for the
garagesubsystem — All / None selection, then raise or close the selected doors - Handles two opener styles automatically per zone:
- Pulse-relay doors (a single momentary contact that toggles the door) — raise/close only fire when the door isn't already where you want it, and press-and-hold the raise/close button forces a pulse regardless of the sensed state (escape hatch for a wrong door sensor)
- Discrete openers (separate open/close/stop, e.g. Konnected) — raise/close send the verb directly and idempotently (no toggle guard); a Stop button appears whenever any door in the fleet supports a real mid-travel stop
- Opener style is detected from the zone's
open_action/close_action/stop_actionattributes — discrete openers carry these (stamped by their driver), pulse-relay doors do not and fall back topulse. See Command Overrides - Renders an interactive zone map when the UI's site space resolves to a floor plan (see Site Spaces → End-user floor plan maps); otherwise falls back to an inline camera feed alongside a row of labeled zone-select buttons, one per door. The map here is the page rather than an overlay over it, so its header carries no close X — only the plan name, and a zoom-out button once you drill into a room
- Each zone-select button shows the door's live status as a colored badge below its label — opening / closing in blue while the door is travelling (from the zone's
operationattribute), open in amber (worth noticing), and closed in green (secured). Relay-only doors that don't report an operation show just open/closed - The inline camera feed is sourced from the
camera_zone_idattribute on thegaragesubsystem (renamed fromcamera_zone; existing rows are converted automatically on upgrade). More than one camera can be listed — as a JSON array ([45, 46]) or as a comma-delimited list (45,46) — and the list order is the order they render in, left to right - Grouping doors under their own camera. With more than one camera on the subsystem, each door can name the one that watches it by setting
camera_zone_idon the zone — the same attribute the door, gate and open/close/stop controls already use for their own inline feed, so the attribute editor's camera-zone picker offers it with no extra setup. The view then splits into a column per camera: that camera's feed on top, the doors it watches directly beneath it. A site with a west camera over three doors and an east camera over four gets two columns instead of one shared feed above all seven tiles- Column order follows the subsystem's camera list, so the camera you list first is the leftmost column
- Doors that name no camera — or name one this subsystem doesn't show — collect in a full-width row beneath the columns rather than disappearing, which makes a missed or mistyped assignment visible
- If no door names a camera, nothing changes: the original single shared feed with one door list underneath renders exactly as before. Grouping needs at least two cameras and at least one assigned door
- Phones and portrait screens stack instead of columning. Each feed renders above its own doors and the whole column scrolls — feed, its doors, next feed, next doors. The door lists always keep their full natural length; the feeds take whatever is left of the screen, up to 30% of its height each and never less than a picture's worth, so on a phone with two cameras and seven doors both door lists sit on screen with a smaller feed above each rather than one feed pushing the doors past the fold. Anything roomy enough for the previous layout — a tablet in portrait — computes a bigger slice and keeps the full 30%. Squeezing the lists instead is what fails: they are sized by their contents, so seven doors across two lists claim most of the height and crush each feed to a sliver. Where even the floored feeds don't fit, the column scrolls, at the cost of a swipe to reach the second group
- Phones stack in both orientations — a phone is short even lying down. Everything else stacks in portrait and uses side-by-side columns in landscape
- The camera-feed fallback is orientation-aware — the camera occupies half the width in landscape and the full width in portrait. Grouped columns sit side by side in landscape, with every feed the same height and every door list starting at the same point regardless of how the doors divide between them. The landscape door row is measured from what the lists actually hold, so tiles that wrap onto a second line — three cameras in a narrow pane, or long door labels — stay inside the row instead of running underneath the feeds; past 45% of the screen height the row stops growing and scrolls inside itself, as before
- On phones the zone-select tiles render at a reduced size so a long door list stays scannable; past 6 doors they return to the standard size (a further-shrunk tile reads too thin) and instead narrow so two tiles fit per row (state-dot themes — classic rect themes keep their fixed-size card). On state-dot themes a reduced tile shrinks its box only — padding, gaps and the dot tighten while the door label, state badge and border keep their readable size, so a smaller tile is a shorter row rather than a uniformly miniaturised card; the inline camera feed also gives up its usual minimum height on phones, shrinking to share the screen with the tile list rather than pushing it out of view. Both thresholds count the doors in that list, so a camera column showing 3 of 7 doors is sized as a 3-door list
/controls/doors
- Door and lock control for the
doorsubsystem — a grid of zone-select tiles (one per door or lock) alongside the selected zone's full control panel - The selected zone's control panel shows an inline live camera feed when the zone carries a
camera_zone_idattribute pointing at the covering camera zone (the attribute editor offers a camera-zone picker; renamed fromcamera_zone, and existing rows are converted automatically on upgrade). Gate and generic open/close/stop zone controls show the same feed - A zone carrying
sip_addressadds a Call button that dials its intercom, and one carryinghide_controlsdrops the lock/unlock (or open/close/stop) buttons entirely — the callbox GEM answers but never releases. See Controls with their own settings - Each tile shows the door's live state and battery level as a colored badge below its label, updated in real time via zone subscriptions:
- closed in green (secured)
- locked in red and unlocked in green — on the doors page only, where the lock states carry a lock indicator's own meaning (red is "you can't go through this", green is "you can") rather than the secured/attention reading the rest of the grid uses. Every other subsystem that reports a lock (gates, covers, lifts) still shows locked as secured green and unlocked as amber
- open / ajar in amber (worth noticing)
- opening / closing / moving in blue (in motion)
- fault / alarm / error / offline in red
- any other state falls back to the generic status colors
- When a zone reports a battery level it is appended to the badge as a percentage (e.g.
closed - 87%) - This live badge now renders for every subsystem that uses the generic zone-grid control (doors, gates, covers, power, water, irrigation, fire, lift, fans, …), driven by each zone's
stateand battery (battery_level/battery) attributes via live subscriptions. A zone that reports neither simply shows a plain label, so subsystems whose zones don't publish state are unaffected - Fan zones that report only a
leveland nostateattribute (switched fan legs on drivers that don't push a named state) derive the badge from the level — above 0 shows on, otherwise off — so the zone tiles agree with the fans page headline count. Fans only: for shades and lights, level means position/brightness, not on/off - In the All group view, a tile whose zone label doesn't already start with its zone group's name leads with the group label (e.g.
Garage FanandPatio Faninstead of two tiles both readingFan). Individual group views keep the original labels. The Lighting and Shades pages do the same in their All view - Each button in the group pane badges how many of that group's zones are active (
3 active), live, so which rooms want attention reads without opening them. A group with nothing active carries no badge. The All group counts each zone once even when it belongs to several groups, and the count ignores any search term in force — it describes the group, not what the filter left standing. Judgement is the same as View Active below. The Lighting and Shades pages badge their group rails the same way, in their own words (3 on,2 open) - The zone pane is titled with the selected group's name (Study) rather than a generic heading, so the title still reads on a phone
- Climate / HVAC zones in the generic zone grid show a different badge: the zone's current temperature and system mode (e.g.
72° Cool) instead of the open/closed + battery badge, driven by each zone'stemperatureandsystem_modeattributes. The badge is color-coded by mode — cool (blue), heat (red), auto (green), dry (orange), fan (cyan), and off/idle (gray). Climate pins on the floor plan read the same way: the temperature sits on the disc in place of the subsystem glyph, and the pin's halo takes the mode color, so which rooms are heating and which are cooling reads across a whole plan without opening a zone (see Climate pins read as thermostats). A climate zone that reports no system mode — a heated floor loop switched on and off, with no setpoint or temperature of its own — falls back to the ordinary state badge and state colours (ongreen,offgrey) used everywhere else, so it shows a live badge and a state dot instead of a blank tile. The mode decides which treatment a zone gets, so thermostats are unaffected - A Map button in the bottom bar opens a floor plan of the subsystem's zones, whenever the UI's site space resolves to one. The plan is derived from site space geometry — nothing is placed by hand — so every subsystem that uses the generic zone grid (doors, gates, covers, power, water, irrigation, fire, lifts, fans, climate, and any custom subsystem) gets one for free. Tapping a pin opens that zone's control, exactly as tapping its tile does; the X in the plan header closes it. Pins print the zone name under the disc wherever the plan has room to print it without running into a neighbour, and touching and holding a pin names it in a callout before the tap commits — sliding off the pin before lifting cancels, so you can check what you are about to open (see Knowing which zone a pin is). Rooms too small to show their pins individually collapse into a count marker that zooms into the room when tapped, and a zoom-out button next to the X returns to the whole floor (see Room clusters and drilling in). Pins carry the same live state as the tile badges, driven by the same subscription and painted from the same color vocabulary, so the plan and the grid can never disagree — a closed door is green on both, an open one amber, a water sensor reporting
leakred (see Pins carry live state). An active pin's halo blooms with the zone's level where the subsystem has one (fan speed, cover position), and shows full for zones that publish none; a room collapsed into a count marker shows its most noteworthy state rather than an average. The plan header names the subsystem after the floor — First Floor Doors — since the same plan serves every control page. The button is hidden when no plan resolves — a UI with no site space has no physical layout to draw. In portrait only one of the bottom bar's two button rows is on screen at a time, so the button appears in both rows; landscape shows both rows and carries it once, on the right. See Site Spaces → End-user floor plan maps - A View Active button in the bottom bar (shown when the grid has more than one zone) trims the tiles to zones in an attention-worthy state, judged from the same state that drives the badge — off/closed/locked/disarmed/idle/offline-style at-rest states, and zones reporting no state at all, are hidden, while a climate zone that reports a mode counts as active unless that mode is off, and one that reports none (a heated floor loop) counts as active on its own on/off state. "Active" means the same thing here as on the controls page — both read one central at-rest state registry (see Tile Status Headlines), so what the filter keeps matches the tile headline count. The same button toggles back to View All and highlights while the filter is engaged, and the filter resets each time the page opens. When the filter is on and no zones qualify, the grid shows No active zones instead of an empty pane
/controls/water
- Water management control
- Leak sensors, shut-off valves
- Flow monitoring
- Each zone's tile is drawn by its own control:
water_temperature(reading only),water_setpoint(reading plus setpoint arrows, for a heater whose power isn't GEM's to switch),water_onoff, orwater_onoff_setpoint. See Picking a water control - The
water_onofftile's two buttons can speak the valve's own vocabulary —on_text/off_textrelabel them (Open / Close on a shutoff) andon_state/off_statesay which reported state lights each one up. Presentation only: the commands sent are stillonandoff. See Controls with their own settings
/controls/irrigation
- Irrigation control for the
irrigationsubsystem — a zone grid of sprinkler valves, stations, and controllers - Uses the generic zone-grid control, so each zone tile carries the live state badge (and battery level where a zone reports one), updated in real time via zone subscriptions
- Zones placed by a sprinkler-controller driver land here automatically when filed under the standard
irrigationsubsystem (see Subsystems)
/controls/weather
- Current conditions, hourly strip, and 7-day forecast for each configured weather location (data from the free Open-Meteo API — no key required)
- The hourly strip follows the selected forecast day: today shows the current hour onward, and picking any other day shows that day's full 24 hours from midnight
- Per-location live radar view (radar.weather.gov map, or a legacy NWS station loop image)
- Locations, ordering, default city, and radar views are configured on the UI editor's Weather tab (see UIs)
/controls/pool
- Pool equipment control
- Temperature, pumps, lights
- Spa control
Custom Control Components
Create custom control components:
- Develop Svelte component in
src/components/controls/ - Add route in
src/routes/controls/ - Create UI control pointing to the component
- Assign to UI
Example Custom Control:
<!-- src/routes/controls/outdoor_audio.svelte -->
<script>
import GemApp from '../../gem/app';
// Custom outdoor audio control logic
</script>
<div class="outdoor-audio-control">
<!-- Custom UI for outdoor audio zones -->
</div>
Then create UI control:
Label: Outdoor Audio
Component: /controls/outdoor_audio
Icon: speaker_outdoor
Icons
Icons resolve theme-first: if the active theme ships the file in its /icons/ directory, that glyph is used on the control tiles and nav; otherwise GEM's built-in icon set is used. A theme only needs to include the icons it wants to restyle.
Standard Icons
lighting- Light bulbaudio- Speakervideo- TV/displayshades- Window blindsclimate- Thermometersecurity- Shieldcamera- Camerapool- Poolirrigation- Sprinklerlock- Padlock
Custom Icons
Upload custom icons via UI > UI Themes page:
- Navigate to UI > UI Themes
- Select theme
- Go to Icons tab
- Upload PNG or SVG files
- Reference by filename in UI Control
Icon Requirements:
- Format: PNG or SVG
- Size: 64x64 to 128x128 pixels
- Style: Should match theme aesthetic
- Filename: lowercase_with_underscores.png
Best Practices
-
Consistent Labeling: Use clear, recognizable names for controls
-
Logical Ordering: Arrange controls by frequency of use:
- Most used first (Lighting, Audio)
- Moderate use middle (Climate, Shades)
- Least used last (Pool, Irrigation)
-
Icon Selection: Choose icons that clearly represent the control function
-
Global Defaults: Use global controls for controls that should appear everywhere
-
UI Overrides: Use UI-specific controls sparingly, only when truly needed
-
PIN Protection: Only PIN-protect controls that need security:
- Security system
- Administrative functions
- Potentially dangerous controls (gates with traffic)
-
Testing: Test controls after adding to verify component exists and loads
-
Enabled State: Disable rather than delete during troubleshooting
Troubleshooting
Control Not Appearing
Check:
- Enabled: Control must be enabled
- UI Assignment: Control is global OR assigned to this UI
- Component Exists: Svelte component file exists at path
- UI Enabled: Parent UI must be enabled
- Role Permission: User's role allows access to this UI
Control Shows Error
Check:
- Component Path: Must start with
/ - Component Exists: File exists at
src/routes/controls/{name}.svelte - Syntax Errors: Component has valid Svelte syntax
- Console Errors: Check browser console for runtime errors
Wrong Icon Displays
Check:
- Icon Name: Matches a file in the theme's
/icons/directory or GEM's built-in icon set - Theme: Correct theme assigned to UI — if the theme doesn't ship the icon, the built-in glyph displays instead
- File Extension: Don't include .png or .svg in icon name
- Browser Cache: Clear cache and reload
PIN Not Working
Check:
- PIN Set: The control row actually carries a PIN — on the UI-specific row or the global (All UIs) row. A control whose PIN is empty (or only whitespace) never prompts
- Right Row: When both a UI-specific and a global row exist for the same component, the panel's own row is checked first
- Throttled: Ten wrong submissions on one connection stop verification answering until the client reconnects
- Leading/Trailing Spaces: Both sides are trimmed before comparison, so stray whitespace in the stored PIN is not the cause
Duplicate Not Working
Cause: Duplicate creates a copy but doesn't auto-save
Solution: After clicking duplicate, configure the new control and save explicitly
Advanced Topics
Conditional Controls
Show/hide controls based on system state:
Not directly supported in UI Controls page. Implement via:
- Custom control component with conditional display
- JavaScript in theme
- Macro that enables/disables controls
Dynamic Control Counts
Display count badges on controls:
Example: Show number of lights that are on
Implement in control component:
let onCount = zones.filter(z => z.power === 'on').length;
Control Templates
For installations with many similar UIs:
- Create template UI with all desired controls
- Duplicate UI
- Modify specific controls as needed
- Saves time vs. configuring from scratch