UIs
UIs (User Interfaces) represent distinct dashboard instances that can be accessed via web browsers, tablets, or mobile devices. Each UI can have unique zones, controls, themes, and pages tailored to specific users or locations.
Overview
GEM supports multiple UIs, allowing you to create:
- Room-specific control panels (kitchen, living room, bedroom)
- Role-specific dashboards (homeowner, guest, installer)
- Device-specific interfaces (wall-mounted tablets, mobile phones)
- Location-specific controls (main house, guest house, pool area)
The auto-generated site UI (a UI linked to a site space) and rendering of any already-built UI are always free. The Custom UI & Widgets module is required to author bespoke UI pages, widgets, themes, custom zone layouts, and the widget gallery. Trials and development builds unlock it, and existing UIs keep working if a license later changes. See License.
Viewing UIs
The main grid displays all configured UIs with the following columns:
- ID - Unique UI identifier
- Name - Internal UI name
- Label - Display name
- Group - UI grouping category
- Site Space - Linked Site Space; when set, the UI is space-linked and its zones come from
zone.site_space_id - Show Child Spaces - When
Site Spaceis set, include zones from descendant spaces too (read-only when no space is linked) - Sort Index - Display order
- Enabled - Whether the UI is active
System UI (ID 0) represents the default UI.
Grid Actions
- Add - Create a new UI
- Edit - Modify an existing UI
- Delete - Remove a UI
- Sort - Reorder UIs by dragging rows
- Reload - Refresh the grid data
Quick Actions (Toolbox)
For each UI row:
- Logout Icon - Force logout all sessions on this UI (prompts for confirmation)
- Notification Icon - Test web push notifications for this UI
- Copy Icon - Duplicate the UI under a new name. This is a deep copy: the UI row plus its attributes, zone assignments, controls, macros, page links, and zone-group links all come along. Pages themselves are shared, not copied — both UIs reference the same pages
- View Icon - Open the UI in a new browser tab
Header Actions
- Reload All - Force reload of all active UIs (prompts for confirmation, refreshes every connected client)
Toolbar Actions
The grid toolbar includes the standard DataGrid actions plus:
- Format Labels (label icon) - Normalize the
labelcolumn viaformatLabel(treats any non-alphanumeric as a word break and capitalizes the first letter of each word). Short all-caps words of 2 letters or less are kept as acronyms (kitchen_TV→Kitchen TV,master_BR→Master BR), but longer all-caps words are Title Cased to tame shouting (KITCHEN→Kitchen,MAIN_LOFT→Main Loft). Mixed-case words preserve their inner casing. Scope follows the current view: with rows multi-selected only those rows are formatted, otherwise every row matching the current filters (across all pages). Shows a confirmation with the count of labels that would change, skips ones already in canonical form, and the change is undoable. Available on any DataGrid backed by a model with alabelcolumn. - Undo (undo icon, also
Ctrl+Z) - Reverts the most recent grid edit. Covers single-cell edits, bulk fan-outs across a multi-cell selection, drag-reorder sort_index changes, and Format Labels runs. Stack is bounded at 20 steps and lasts only as long as the grid is mounted.
Creating a UI
To create a new UI:
- Click Add in the grid toolbar
- The UI editor opens with two tabs: UIs and Attributes
- Configure the UI properties (see sections below)
- Click Save
UI Editor
UIs Tab
Configure the basic UI properties:
Basic Information:
- Name - Internal identifier (lowercase_with_underscores)
- Label - Display name shown in UI selector
- Group - Optional grouping category
- Enabled - Toggle UI active state
- Sort Index - Display order in UI selectors
Examples:
Name: kitchen_panel
Label: Kitchen Panel
Group: main_floor
Enabled: Yes
Settings Tabs — Shared Defaults vs Per-UI Overrides
The Display, Pages & Navigation, Intercom & Voice, Screen & Gallery, Light Scenes, and Weather tabs edit UI settings (attributes). Settings are layered: values stored on ui 0 are shared defaults for every UI, and a row on a specific UI overrides the shared default for that UI only.
Shared defaults are edited in one place — the Shared Settings button above the UIs list opens the shared-defaults editor (only the settings tabs; no name/label/space fields). Saving there updates every UI that doesn't override the changed values.
Individual UI editors only store differences. The form shows the merged result, and saving writes just the fields that differ from the shared defaults:
- Change a field → save stores a per-UI override; the field gains a blue override badge.
- Set a field back to the shared value (or clear it) → save removes the override so the UI follows the shared default again.
- Fields matching the shared defaults are never written — opening and saving a UI (or creating a new one) does not copy shared defaults into per-UI rows.
- Click the ✕ on an override badge to revert that field to the shared value immediately.
Toasts list exactly which overrides a save created and which fields reverted to the shared defaults.
A third layer sits above both. A handful of cosmetic settings — wallpaper, theme, homepage, weather location — can also be overridden per user, and that override wins over the per-UI value for whoever is signed in. See Personal Settings; a site with no per-user overrides resolves exactly as described above.
Override Cleanup — sites that accumulated per-UI copies of shared values under the old save behavior will see an Override Cleanup section in the shared-defaults editor listing redundant overrides (per-UI rows identical to the shared default). Removing them changes nothing immediately, but lets those UIs follow future changes to the shared value. Only settings managed by this form are considered — custom UI attributes are never touched.
Light Scenes Tab
Controls how the Lighting control page (/controls/lighting) presents scenes:
- Disable scene editing — hide scene create/edit controls; users can still recall existing scenes.
- Disable light scenes — hide scenes entirely. The lights page always opens on the Groups tab and the Default Lights View setting is ignored.
- Default Lights View — which tab the lights page opens on when scenes are enabled: Scenes (default) or Groups. Choose Groups to land on per-zone controls first even when scenes exist; when scenes are disabled this is ignored.
- Auto-Scene Labels — display names for the built-in auto-scenes: Soft, Bright, and Off.
Weather Tab
- Weather Locations — named locations available to weather widgets and pages, stored as
latitude,longitude. Drag the handle to reorder rows — the order is how cities appear on the weather page. Star a location to make it the default shown first (clicking the active star clears it; with no star, the first location is used). Each row has a Map button that opens a map picker (search by address or zip, or click to drop a pin); add a new location by typing a name and clicking Pick on Map. Coordinates can also be edited inline. Weather data comes from the free Open-Meteo forecast API — no API key or service URL is needed. - Radar (per location row) — opens the radar-view picker: a live radar.weather.gov preview centered on the location. Adjust the zoom slider (and coordinates if needed) to frame exactly the area that matters, or open the site in a new tab, position the map precisely, and paste the address-bar URL back in. Saving stores a radar.weather.gov map URL for that location; the weather page then embeds the live radar map zoomed to the saved view instead of a wide station image.
- Weather Radar Views — the raw location → radar value map. Values are radar.weather.gov map URLs (set by the Radar button) or legacy NWS station codes like
FWS, which still display via the station's loop image.
Attributes Tab
UI-specific attributes control behavior and appearance:
Common UI Attributes:
Theme:
theme- Theme name (set via UI Themes page)- Controls all visual styling
Gallery:
gallery_photo_path- Path to gallery images folder- Used for screensaver/photo frame mode
- Example:
/mnt/photos/family
Screensaver:
screen_timeout- Idle time before screensaver (seconds)screen_timeout_action- What to do on timeout:not_set- No actiongallery- Show photo gallery/screensaverlogout- Log out usergoto_page- Navigate to specific page
Navigation:
default_page- Page to show on loginhome_button_action- What home button doeshide_back_button- Hide the in-app back button. By default every layout shows one whenever there is in-app history to return to. In the classic top bar (Prodigy, Carbon, and any othertoplayout) the back button shares its slot with the climate shortcut: the pill shows the indoor or outdoor temperature while there is nowhere to go back to, and swaps to a back arrow once the user drills into a page. In rail/tab-bar layouts (e.g. the Ember theme) it appears next to the menu button in the page header. Either way it walks back through visited pages and hands the slot back (or hides itself) when the history is empty. History is per-session, skips transient pages (login, PIN prompts, loading), and clears when roaming to another room's UI. In portrait, pane switches inside split (bifold) pages also enter history — drilling from one pane to the other (e.g. picking a scene on the Lights page) can be undone with back before it walks to the previous page; a pane's history is dropped when you leave its page. Set it from the Hide Back Button switch in the UI editor's Display Settings, or on the shared defaults (UI 0) to hide it on every UInav_layout- Navigation chrome layout for this UI:top(classic top bar) orrail(left icon rail with page header, landscape only). Overrides the theme'snav_layout; leave empty to inherit from the theme (see the theme contract)control_order- User-saved order for the controls grid and nav rail / tab bar shortcuts, written by the press-and-hold reorder gesture on the controls page (see UI Controls). Delete it to restore the integrator-defined sort-index orderinglock_control_order- Prevent users from reordering controls by press-and-hold on this UI (Lock Control Order switch in the UI editor). Set it on the shared defaults (UI 0) to lock every UI; a per-UI value overrides. Enforced server-side, not just hidden in the client
Layout:
zones_per_row- Number of zones per row in grid layoutscompact_mode- Reduced spacing for smaller screensshow_labels- Show/hide zone labels
Sorting:
ui_group_sort- How zone groups are sorted in control pages. Set toalphabeticalto sort groups by name instead of the default sort index order. The aggregate All group is not a peer of the others — it is a view over all of them — so it stays pinned first under either setting.
Notifications:
web_push_enabled- Enable browser push notificationsnotification_sound- Alert sound file
Camera Streaming:
rtsp_release_ms- Grace window in milliseconds before the client RTSP socket pool tears down an idle camera connection. Default5000. Higher values keep streams warm across UI transitions (e.g. opening/closing a zone modal) at the cost of holding sockets open after navigation; lower values release server-side ffmpeg decoders sooner but may re-handshake on quick re-opens.
Lutron Keypad Widget Binding:
lutron_control_station_id- Station id consumed by the Lutron Keypad widget whenever no keypad is picked in the widget's own settings (or its Use UI attribute option is on, which forces this attribute even when one is picked). Lets one widget definition follow whichever UI it's rendered under instead of hard-coding a station per UI page. Populated automatically by thelutron_qsxdevice'ssync_areascommand for any UI whosesite_space_idmatches a Lutron area that has control stations. When edited manually, the attribute editor shows a station picker (with area name) sourced live from thelutron_qsxdriver instead of a free-text id field.lutron_control_station_2_id,lutron_control_station_3_id, ... - Additional stations from the same area, written when an area has more than one keypad. Stations are sorted by name (then station id) so re-syncs land the same physical keypad in the same slot.- The Lights control page (
/controls/lights) also reads these attributes: when any are present (and thelutron_qsxdevice still exists), a Keypad segment joins Scenes/Groups in the page's left-pane switcher and becomes the default view, rendering every station in slot order.
On every connect, the lutron_qsx driver walks the processor's areas and subscribes to every LED (/led/{href}/status) and every keypad device (/device/{id}/button/status/event — one subscribe per device covers all of its buttons via LEAP's MultipleButtonStatusEvent). LED and button state goes live as soon as the device connects, without running sync_areas or syncZones first, and new keypads are picked up automatically after a project republish. The led_subscriptions (LED hrefs) and button_subscriptions (keypad device ids) attributes are persisted so the next reconnect's fast path can restore subscribes without re-discovering, and remain available as optional override lists for installs that need explicit subscribes.
AI Chat (Ask GEM):
hide_ai_chat- Hide the Ask GEM button from this UI's top nav. Default off (button shown).- The button only appears when an Anthropic API key is configured server-side (
ai.anthropic_api_keyingem.jsonor theanthropic_api_keysystem attribute). Tapping it opens an in-page chat that lets users control the home in plain English (lights, climate, shades, AV, scenes), with optional spoken responses on devices that support the Web Speech API.
Voice requests carry the signed-in user's authority, not the assistant's. Every command it sends is checked twice before it reaches a device:
- Elevated devices and macros are refused for a non-elevated user, exactly as the same request would be from a button or the REST API. A garage or gate backed by a device flagged Elevated cannot be opened by voice from an account that couldn't open it by tapping.
- The panel's own scope is enforced — zones and the devices they reach. A phrase that resolves to a zone this UI does not include, or to a device outside the UI's reach (including an AV source dedicated to another room's AV zone), is refused rather than dispatched — even when the same user could reach that target from another UI. A spoken command is contextual to the panel it was spoken at, so a guest tablet can't be talked into another part of the building. UIs with no zone scope resolved at all (no UI Zones and no Site Space) are unaffected.
- Changing the site mode needs an elevated role. "We're home" runs the
mode's whole transition action set — on a typical configuration that is disarm plus entry-door
unlock — so it is held to the same requirement as the
set_site_modefunction itself, refused with an explanation rather than silently claiming the house changed. Asking what the mode is, or which modes exist, stays open to every role.
Some things Ask GEM can read carry text nobody at the site wrote — a calendar invite title, a track or app name off a streaming service, a hostname that joined the network, a device's own error string, or a camera scene described in words (a sign held up to a lens is text arriving through a sensor). Once any of those has been read while answering a request, the assistant refuses to operate anything physical for the rest of that request and says so.
The refusal covers doors and gates, raw device commands, running a macro, whole-house sweeps, site mode, the water shutoff, and the deferred forms of all of them (a scheduled unlock is still an unlock). Comfort controls — lights, shades, volume, climate, colour — are deliberately not covered, and neither is "lock up", which only ever locks and arms.
The cost is a genuine two-part request ("what's on my calendar, and set the house to away"), which is answered and then declined with a prompt to ask directly. Asking again on its own works normally, because each request starts clean. Refusals are recorded in the server log.
When a user asks for something like "turn off the kitchen lights" or "dim the master bedroom to 30%", Ask GEM resolves the room phrase by matching it against the name, label, and group of every UI first. If a UI matches and is space-linked, its Site Space (plus descendants when include_child_spaces is on) defines the scope; if it's a manual UI, the scope is its UI Zone list. If no UI matches, the assistant falls back to a substring match on zone label/name. Naming a UI after the room it controls (kitchen, master_bedroom, downstairs) gives the assistant a much sharper sense of "where" — without it, ambiguous phrases like "downstairs" depend on every relevant zone happening to contain that word.
Ask GEM understands deferred actions ("turn off the patio lights in 30 minutes", "in an hour set the kitchen lights to 20%", "lock the front door in 5 minutes") and queues them in-memory, capped at 8 hours per timer and 20 zones per fan-out. Users can also cancel pending timers ("cancel that timer", "never mind", "cancel the patio timer", "cancel all my timers") or list what's pending ("what timers do I have set?"). Timers are sandboxed per user — one user's pending timers can't be seen or cancelled by another. Pending timers do not survive a server restart; for recurring/persistent rules use Macro Schedules instead.
Ask GEM also arms one-shot condition rules — the "when X happens, do Y" phrasing that no timer covers ("turn the fan on when it gets above 78", "when the garage opens turn on the driveway lights", "turn the heater off once the bedroom hits 70", "when the basement humidity goes over 60 turn on the dehumidifier"). It watches one zone attribute (state by default, or temperature / humidity / level / occupied for sensors), and the first time the condition holds it dispatches the action — a single zone, or a lights/shades/fans/power fan-out capped at 20 zones — then disarms itself.
Three behaviours are worth knowing:
- If the condition is already true, it acts immediately and says so ("It's already 80 in there, so I turned the fan on") rather than silently waiting for a change that already happened.
- It fires once. An unfired rule also expires on its own after 8 hours by default (24 hours maximum), so a condition that never occurs doesn't linger.
- Cancellation is shared with timers. "Cancel that", "what am I waiting on?" and "cancel all my timers" list and clear armed rules alongside pending timers, and the listing distinguishes the two.
Rules live in memory and are sandboxed per user, with the same restart caveat as timers. A standing rule the homeowner wants permanently ("every time the garage opens…") is a real Attribute Trigger and integrator configuration — the assistant says so instead of arming a one-shot.
Ask GEM can put a light, shade, fan, or outlet back to the value it held before its most recent change ("undo that", "put it back", "never mind, put the lights back", "restore the living room lights"). It reads attribute history for level and state, takes the value recorded immediately before the last change, and dispatches the matching command — so the user never has to remember what the old setting was. The default lookback is 15 minutes (max 4 hours for phrasings like "put it back to how it was this morning"); anything older is reported as nothing to undo. A bare "undo that" with no room named undoes only the single most-recently-changed zone, not the house; naming a room fans out to at most 6 zones.
This is hard-scoped to lights, shades, fans, and power. Doors, locks, security, climate, and AV are refused by design so an ambiguous "undo" can never unlock a door, disarm an alarm, or swing a thermostat — the user is asked to state the action instead. Requires attribute history to be enabled for the zones involved.
Ask GEM handles the cross-category shut-down in one sweep ("turn everything off", "shut down the theater", "we're leaving, turn it all off", "kill everything upstairs", "all off"). Naming a room scopes it the same way room resolution works above; naming nothing means the whole house.
By default it covers lights, fans, and AV — AV zones have their source cleared, which single-subsystem commands can't do at all. Outlets are opt-in ("everything", "all the outlets") because a plug may be feeding a fridge or sump pump, and shades are only closed when the user asks for that as part of it. Zones already off are skipped, so the spoken reply reports what actually changed. Capped at 30 zones; a casual whole-house request is previewed for confirmation first.
Climate, security, doors, locks, gates, cameras, irrigation, and pool equipment are never touched, whatever phrasing is used — the user is routed to the specific request instead. As with the other sweeps, a curated goodnight / leaving / all_off macro on the UI is preferred over the generic version, since it's what you programmed and may cover more.
Ask GEM can lock every accessible door and arm the alarm in one breath ("lock up", "secure the house", "I'm leaving — lock up", "going to bed, lock up"). Defaults to stay mode for "lock up" / "going to bed" phrasings; use "I'm leaving" / "arm the alarm" for away mode, or "just lock the doors" to skip arming. The sweep is hard-scoped to door/lock/gate and security zones the user can already see on this UI, capped at 30 of each, and skips zones already locked or armed (so re-asking is a no-op). Garage zones are included by default — say "just the interior locks" to exclude them. Unlocking and disarming are intentionally not voice-controllable — for safety, the assistant will direct the user to the touchscreen instead. If you have a curated goodnight / lock_up / leave_home macro on the UI, the assistant prefers running that over the generic sweep so any associated lights/scenes fire too.
Ask GEM answers questions about the access configuration — who currently holds a valid door credential, whose is about to lapse, and whose already has ("who has access to the house?", "who can get in?", "is the guest code still active?", "when does the cleaner's access expire?", "does anyone have a temporary code?", "whose code expires this week?", "can the neighbor still get in?"). Naming a person makes the answer a direct yes/no about them; naming a door ("who can open the gate?") scopes it to that reader's rules. This is distinct from "who came in?" / "did the cleaner come today?", which read the access log — entries that already happened.
The reply reports names, status, doors, and expiry windows only — it never contains a PIN, card number, or any other credential value; the underlying query doesn't even read those columns. It is also strictly read-only: asked to add, extend, or revoke someone's access, the assistant directs the user to the admin app (Users, Access Control) instead of attempting the change.
Ask GEM operates garage doors, gates, and lifts directly ("open the garage", "close the garage door", "shut the garage", "open the driveway gate", "hit the garage button", "stop the garage door", "close both garage doors"). Commands go through the normal zone command path, so relay state mapping and any per-zone <verb>_action override apply; openers that expose only a single momentary button are retried with toggle when they reject a discrete open/close.
What can be opened is decided by the zone's subsystem, never by its label. Zones on garages, gates and lifts are openings; zones on doors and locks are latches, and on access-control drivers open means unlock — so a deadbolt called "Garage Entry Door" is not openable by voice no matter how it is named. If a zone genuinely is an overhead door or gate but has to live on doors, set its door_family attribute to garage, gate or lift; the refusal names that attribute so the fix is self-serve. See Subsystems.
Opening is never guessed. If more than one opening could match and the user named none, the assistant asks which one instead of picking — so "open the garage" in a three-bay garage becomes a question, not a gamble. A house with a single garage needs no qualifier. Closing, stopping, and locking travel in the safe direction and may sweep a scope (capped at 10 zones); zones already open/closed/locked are skipped so the spoken reply reports what actually moved.
Unlocking is not available at all — there is no unlock verb, and open/toggle are refused on deadbolt and latch zones. As with "lock up" above, the user is directed to the touchscreen or keypad. For a deferred version, users can say "close the garage in 10 minutes" (a delayed command) or "open the gate for 5 minutes" (auto-reverses).
Ask GEM operates the whole-home shutoff valve and reads its position ("shut off the water", "kill the water", "close the water valve", "turn the water back on", "is the water shut off?", "is the water valve open?"). Scoped to water and plumbing valve zones the user can already see on this UI — irrigation and sprinkler valves are excluded by design, since those are the sprinkler request instead.
Shutting off travels in the safe direction and may sweep every accessible valve (capped at 5); a valve that rejects close is retried once with off, since some drivers expose only on/off. Re-opening does not. It needs an unambiguous target — the user names the valve, or the house has exactly one — and it is refused outright while any leak sensor is still wet, with the assistant naming the sensor and asking before it will try again. Both directions are idempotent, so "shut the water off" against a closed valve answers "The water was already shut off" rather than re-sending.
Asking about the water reports valve position, whether water is actually flowing, and any active leak signal in one sentence. Consumption questions ("how much water have we used today?", "what's the water pressure?") are answered from the meter instead, and a general "any alerts?" still sweeps every safety sensor. When no valve is configured the assistant says so plainly rather than claiming it shut anything off. For a timed version, "shut the water off for an hour" becomes a delayed command.
See UI Themes and Gallery for theme and gallery configuration.
Accessing UIs
Direct URL Access
Access a specific UI by URL parameter:
https://gem.local/?ui_id=1
https://gem.local/?ui_id=5
Or by UI name:
https://gem.local/?ui=kitchen_panel
https://gem.local/?ui=master_bedroom
UI Selector
When multiple UIs exist, a UI selector appears allowing users to switch between UIs. The selector is a single tile grid that drills down into Site Spaces, UI groups, or a flat All view, with a breadcrumb back button at the top of any sub-view.
Root tiles mix three sources, sorted together by sort_index — entries without one sort last, ties break alphabetically (set ui_sort: alphabetical in app settings to sort by label only). Every drilled sub-view sorts by the same rules:
- Top-level site spaces — each space with a linked UI lands you in it on tap, and spaces with child spaces show a
›suffix so you can drill in. Spaces with both a UI and descendants act as both — tap navigates, hold-to-set-default still works. - UI groups — every distinct
groupvalue from non-space-linked UIs becomes a drill-down tile (suffixed with›). Group order honorssettings.ui_groupsif set, otherwise falls back to the smallest membersort_index. - Ungrouped manual UIs — UIs with no
site_space_idand nogroupshow as direct tiles at root. - All — a flat list of every UI, always pinned at the top of root when any UIs exist.
A segmented control, centered in the picker header, offers the same jumps without drilling: All first, then one segment per top-level space with descendants and per UI group. Segments follow exactly the tile grid's order — the same comparator sorts both, so the bar and the grid never disagree. Tapping a segment swaps the grid to that scope; the bar shows only at root, hiding while a search is active or a sub-view is open.
Drill-down pushes onto a stack so the back button (‹ Home / ‹ Parent Name) pops one level at a time. Searching collapses the view to a flat filtered list across every UI regardless of current scope; clearing the search restores the drilled scope.
Hold any UI tile for 2 seconds to set it as the default UI for that device (same behavior as before — stored against gem_client_id + user agent).
The selector toolbar also exposes per-device actions that don't require admin access:
- My Settings — opens the user's own Personal Settings page (wallpaper, theme, homepage, weather location). Only shown when a real account is signed in; a shared panel on a permanent session has no personal layer, so the entry is hidden
- Clean Screen — temporarily disables touch input so the screen can be wiped down
- Request Notifications — prompts the browser for web push permission
- Show / Hide Media Nav — toggles the media navigation bar
- Zoom — opens a modal that scales the entire UI between 50% and 150% via
document.documentElement.style.zoom. The chosen value persists inlocalStorage(gem_ui_zoom) on that device only and is re-applied each time a UI loads. Useful on smaller iPhones where standard controls get cut off; reset to 100% to clear the override. - Mobile App — opens the QR code modal described in Mobile App: App Store and APK downloads plus an External and an Internal setup code, so a device can be pointed at the cloud URL or the local network URL
- Change GEM Url — on iPad / Android / Capacitor shells and the native mobile app, lets the user point the app at a different server. In the native app this clears the stored server URL and returns to the first-launch setup screen
- Device Settings — legacy Android shell only; opens the host app's native settings screen
Default UI
UI with ID 0 is the system default. Users without a specific UI assignment see this UI.
UI Components
UIs are composed of:
- UI Controls - Navigation buttons/pages (see UI Controls)
- UI Zones - Zones assigned to this UI (see UI Zones)
- UI Macros - Macros accessible from this UI (see UI Macros)
- UI Pages - Custom pages with widgets (see UI Pages)
- Theme - Visual styling (see UI Themes)
Space-Linked vs Manual UIs
Every UI is either space-linked or manually composed. The distinction determines where its zones come from.
In a well-organized install, most UIs can be built simply by assigning zones to Site Spaces — the space tree drives everything automatically. Reach for manual ui_zone composition only when the UI doesn't map to a physical space (building-wide dashboards, security panels, power-user consoles).
Space-Linked UIs (site_space_id is set)
The UI is bound to a Site Space via site_space_id. Zones are pulled live from zone.site_space_id — no ui_zone rows are needed. Moving a zone into or out of the space makes it appear or disappear in the UI automatically.
include_child_spaces: false— show only zones in that exact space (default for rooms, kitchens, etc.)include_child_spaces: true— include descendants as well, with each sub-space becoming a zone group (default for containers: building, floor, wing). The control pages also prepend an All group that aggregates zones across every child space.
Space-linked UIs can still take explicit UI Zone and UI Control rows — they're layered as overlays on top of the inherited set. Use them to add a zone or control that isn't in the space (e.g. a shared front-gate camera on every room panel, or a room's AV zones when they aren't space-tagged), or to suppress an inherited control by adding a ui_control row with the same component and enabled = false.
Overlay zones are honored everywhere the UI's zone set is resolved — the individual-control views, the AV zone list and media bar (including the default AV zone), the floor-plan map, and the command scope gate — so an overlay row behaves exactly like a zone the space contributed.
Create a space-linked UI from the Site Spaces page with Create UI, or set site_space_id directly when creating a UI.
The side menu for a space-linked UI is auto-generated from the space's zones:
- One control button per subsystem with zones in scope (lights, shades, climate, security, …).
- AV is split: an Audio button appears only if at least one in-scope AV zone has
av_type = audio, and a Video button only if at least one hasav_type = video. Spaces with no AV zones get neither. - A Spaces button is appended whenever the space has child spaces, providing drill-down navigation to each child space's UI. Children without their own UI but with descendants that do are shown as drill-down tiles (label suffixed with
›) — tapping descends one level deeper, and a breadcrumb with a back button lets you pop back out.
Space-linking is also what gives a UI floor plan maps: whenever the linked space (or an ancestor of it) has a plan image, every subsystem page offers a Map view with a pin per zone, derived from the space geometry. Manual UIs have no physical layout to draw, so the Map affordance never appears on them. See Site Spaces → End-user floor plan maps.
Manual UIs (site_space_id is null)
Zones come from explicit UI Zone rows and controls from UI Control rows. Nothing happens automatically. Use for:
- Building-wide dashboards ("All Lights", "Security Panel", "HVAC Overview")
- Power-user management consoles that mix zones from anywhere
- Guest or role-restricted UIs with hand-picked zones
Common Workflows
Creating a Room Panel
For a kitchen wall tablet:
- Click Add
- Configure:
Name: kitchen_panelLabel: Kitchen PanelGroup: main_floorEnabled: Yes
- Save and note the UI ID
- Navigate to UI > UI Zones and assign kitchen zones
- Navigate to UI > UI Controls and add relevant controls
- Navigate to UI > UI Themes and assign a theme
- Access via:
https://gem.local/?ui=kitchen_panel
Creating a Mobile UI
For homeowner mobile app:
- Click Add
- Configure:
Name: mobile_homeownerLabel: Mobile - HomeownerGroup: mobileEnabled: Yes
- Save
- Assign all necessary zones (all subsystems)
- Add all controls
- Set compact theme optimized for mobile
- In Attributes, set:
compact_mode: truezones_per_row: 2
Creating a Guest UI
For limited guest access:
- Click Add
- Configure:
Name: guest_panelLabel: Guest PanelEnabled: Yes
- Save
- Assign only guest-accessible zones:
- Guest bedroom lights
- Guest bathroom lights
- Common area lights
- Temperature control
- Add limited controls (no admin, no advanced features)
- Create guest user account
- Restrict guest role to only this UI
Duplicating a UI
To create a similar UI:
- Edit existing UI
- Note all settings
- Create new UI
- Manually configure with similar settings
- Or export/import UI configuration (advanced)
Testing UIs
Open UI
Click the View icon to open the UI in a new browser tab:
- Tests UI functionality
- Verifies zone assignments
- Checks theme application
- Validates controls
Test Notifications
Click the Notification icon to test web push:
- Sends test notification to connected clients
- Verifies notification configuration
- Tests notification delivery
Force Logout
Click the Logout icon to disconnect all clients:
- Useful for testing login behavior
- Forces UI reload
- Clears client cache
- All users connected to this UI will be logged out
UI Groups
Group UIs by category for organization:
Common Groups:
main_floor- Main floor panelssecond_floor- Second floor panelsmobile- Mobile appsguest- Guest interfacesservice- Service/installer interfaces
Groups appear in UI selectors and help organize large installations. Removing a group prompts for confirmation — UIs assigned to that group will become ungrouped.
Multiple UIs vs. Single UI
When to Use Multiple UIs
Good Reasons:
- Different physical locations (room panels)
- Different user types (owner vs. guest)
- Different devices (tablet vs. phone)
- Different themes per location
- Site-specific configurations (multi-tenant)
Example:
UI 1: Kitchen Panel (kitchen zones only, bright theme)
UI 2: Bedroom Panel (bedroom zones only, dark theme)
UI 3: Mobile App (all zones, mobile-optimized theme)
UI 4: Guest Panel (limited zones, simplified interface)
When to Use Single UI
Good Reasons:
- Simple installations
- Consistent experience across devices
- Centralized management
- Minimal configuration
Use Roles Instead:
- Different permissions for different users
- Role-based access control handles restrictions
- Single UI, multiple user roles
Security and Access
Role-Based UI Access
Control which users can access which UIs using Roles:
- Navigate to Security > Roles
- Edit or create a role
- In User Interfaces permissions:
- Allow: Specific UI IDs
- Deny: Restricted UI IDs
- Assign role to users
Example:
Guest Role:
Allow UI: guest_panel
Deny UI: all others
Result: Guests can only access guest_panel
Site-Based UI Access
For multi-site installations:
- Create UIs per site
- Assign UIs to appropriate sites (via attributes)
- Restrict users to specific sites
Advanced Topics
UI Reloading
Reload All Button:
- Broadcasts reload command to all connected clients
- Forces UI refresh
- Useful after:
- Theme changes
- Major configuration updates
- Zone additions
- Control modifications
Individual UI Logout:
- Disconnects clients from specific UI
- Triggers automatic reload on next access
- Less disruptive than Reload All
Web Push Notifications
Enable browser push notifications:
- Edit UI
- In Attributes, set
web_push_enabled: true - Clients will be prompted to allow notifications
- Test with notification icon in grid
Requirements:
- HTTPS connection (SSL enabled)
- Browser support (Chrome, Firefox, Safari 16+)
- User permission granted
Dynamic UI Loading
UIs can load different configurations based on:
- Query parameters
- User roles
- Time of day
- System mode (home, away, vacation)
Configure via UI attributes and custom logic.
Embedded UIs
Embed GEM UIs in other applications:
<iframe src="https://gem.local/?ui=kitchen_panel&embed=true" />
Embed parameter removes navigation chrome for cleaner integration.
GEM sends X-Frame-Options: SAMEORIGIN, so a UI can only be framed by a page served from the same origin as the controller. A dashboard hosted on another hostname gets a blank frame, and the browser console reports the refusal.
Serve the embedding page from the controller (or from behind the same reverse-proxy hostname), or link out to the UI in a new tab instead. The real-time socket applies the matching rule to its own connections — see Real-time connection origins if a framed UI loads but never updates.
Troubleshooting
UI Not Showing Zones
Check:
- Zones assigned to UI in UI > UI Zones
- Zone groups configured correctly
- Subsystem enabled
- Zones enabled
- Device online
UI Shows Wrong Theme
Check:
- Theme attribute set for UI
- A personal theme override on the signed-in account — a personal theme wins over the UI's, and takes effect once the session comes up, so the screen re-themes shortly after login
- Theme files exist in
/themes/{theme_name}/ - Browser cache cleared
- CSS file is valid
Cannot Access UI
Check:
- UI enabled
- User has role permission for UI
- Correct URL or UI parameter
- Network connectivity
- GEM server running
UI Not Reloading
Try:
- Force browser refresh (Ctrl+F5)
- Use Logout icon to disconnect
- Clear browser cache
- Use Reload All button
Intercom Has No Microphone
An intercom call connects and the panel plays incoming audio, but nothing is heard at the far end, and the console logs sip: no media devices — insecure origin, microphone unavailable.
Browsers only expose a microphone in a secure context — HTTPS, localhost, or the mobile app. A panel opened over plain HTTP by hostname or IP (http://192.168.1.50/?ui=kitchen_panel) has no microphone available at all, regardless of any permission prompt.
Fix: enable SSL on the controller and open the panel over HTTPS. See SSL / TLS — if the certificate comes from GEM's own CA, install the CA on the panel first so the browser trusts it. The rest of the intercom (ringing, incoming audio, hanging up) works on an insecure origin; only outbound audio is affected.
Offline Behavior
The GEM web UI is cached on each device by a service worker, so panels and phones keep a working interface when the network or the controller is briefly unreachable:
- On first load (and again after every GEM update) the full UI shell — the application bundle, stylesheets, fonts, and icons — is downloaded into the browser's cache automatically. No page browsing is required to "warm" it.
- A device that reboots while the controller is offline still loads the UI from its cache, on any page, and reconnects automatically once the controller is reachable again.
- Live control requires a connection to the controller. Offline, the UI opens and shows its disconnected state; commands and live values resume when the connection returns.
- Theme styling and images are cached as they are used, so a panel shows its normal theme offline after it has displayed it online at least once.
- The cache is replaced as a unit when a new GEM version is installed; devices must be online briefly after an update to pick up the new interface.
If a device instead shows "offline and not cached", its cache predates this behavior (or was cleared) — load the UI once while online to restore it.
Reconnect and Resync
Live updates are not replayed: a change broadcast while a panel's connection was down is gone, so reconnecting alone would leave the panel showing pre-outage state (a stale source in the media nav, a wrong volume) until the next change happened to arrive. Three mechanisms close that gap:
- State resync on reconnect — once the connection is re-established, the panel re-reads the current values it displays live (source, volume, mute and related room AV state) and applies them. A source change made from another panel while this one was offline shows up moments after the connection returns.
- Source page reload on reconnect — a source view that was open (or was opened) while the connection was down loads nothing: its one-time reads all fail at once, leaving the page empty rather than merely stale — no Roku app grid, no Spotify or Kaleidescape browse list, no DirecTV widget, the generic "Airplay" name instead of the source's own, and for an Amazon Music, Pandora or SiriusXM source not even a panel, since which one to draw is decided by a lookup that failed. Those reads are re-run automatically when the connection returns, so the page fills itself in instead of having to be navigated away from and back.
- Dead-connection detection — a connection can die without the panel noticing (a Wi-Fi roam, a NAT timeout): the socket still looks connected but nothing arrives. Panels probe the server every 15 seconds; after two consecutive unanswered probes (about 30 seconds) the panel drops and re-establishes the connection itself, which triggers the same resync. This matters most for wall panels, whose screens never sleep and so never hit the resync that runs when a display wakes.
A network blip never signs a panel out — a panel logs out only when the server explicitly reports its session is no longer authenticated.
Related Documentation
- UI Controls - Adding navigation buttons and pages
- UI Zones - Assigning zones to UIs
- UI Macros - Adding macros to UIs
- UI Pages - Creating custom dashboard pages
- UI Themes - Styling and theming UIs
- Personal Settings - Per-user wallpaper, theme and homepage overrides
- Gallery - Configuring photo galleries
- Roles - Controlling UI access