Reference

Configuration

This page is a reference for power users. Home Screens stores everything as JSON files, but you almost never need to touch them, the editor manages all of this for you. This page exists to document the schema for scripting, external tooling, or debugging.

Home Screens stores all configuration as JSON files on disk. The main config file is data/config.json; a few feature-specific data files (meals, chores, rewards) live alongside it. There is no database, every file is read and written directly by the API with atomic writes (temp file + rename) to prevent corruption during power loss.

Data files

FilePurposeAPI
data/config.jsonScreens, modules, profiles, global settings, multi-display registry/api/config
data/secrets.jsonAPI keys for external integrations (weather, calendar, photos, etc.)/api/secrets
data/auth.jsonPassword hash and session secret for editor authentication(internal)
data/meals.jsonMeal library, weekly plan, checked-off grocery items, household meal settings/api/meals/data
data/family.jsonShared family members, family groups, and legacy identity aliases/api/family
data/chores.jsonChore definitions/api/chores/data
data/chore-completions.jsonChore completion history (last 90 days)/api/chores
data/rewards.jsonReward definitions, point balances, redemption history/api/rewards/data
data/google-tokens.jsonGoogle Calendar OAuth tokens(internal)
data/icloud-accounts.jsoniCloud account credentials (app-specific passwords) for calendar sync/api/icloud/accounts
data/todos.jsonShared to-do lists (items, due days, people, repeats)/api/todo/lists
data/timetables.jsonSchool timetables: schools and bell times, the shared subject list, one week per person/api/timetables
data/school-holidays.jsonLast-good copy of the school and public holiday dates looked up for a region/api/timetables/holidays
data/routines.jsonSaved timer routines (the steps, not a running timer)/api/timers/routines
data/timer-session.jsonThe one timer running right now, as a snapshot plus timestamps so displays can count down on their own/api/timers/session
data/google-picker-tokens.jsonGoogle Photos Picker tokens, kept separate from the Calendar tokens above(internal)
data/onedrive-tokens.jsonOneDrive photo sign-in tokens(internal)
data/backup-state.jsonLast-backup and last-dismissed timestamps behind the backup reminder/api/backup
data/port.confCustom server port (preserved across upgrades)(internal)
data/plugins/Installed plugin bundles and manifests/api/plugins/*
data/plugin-tokens/Per-plugin account tokens from server-side auth adapters/api/plugins/auth/*
data/plugin-secrets/Per-plugin secrets you enter yourself, kept outside data/plugins/ because a plugin upgrade replaces that folder wholesale/api/plugins/secrets
data/custom-icons/The family's own icons: index.json lists them, and each picture sits beside it as a .webp named by its content/api/custom-icons

Chore definitions and chore completions are two separate files. Copying only data/chores.json leaves every completion (and therefore every earned point) behind.

Other files under data/ are written and managed by the app and are not meant to be edited by hand: backups/, thumbnails/, kiosk.conf, telemetry.json, background-cache.json, weather-today.json, audit.log, the update records (auto-update-setting.json, auto-update-state.json, update-notification-state.json, upgrade-failed.json, restart-needed.json), the Pi program records (system-packages.json, system-package-holds), the change journal behind family and restore writes (family-transaction.json), and the records of the one-time move to a shared family list (family-migration.json, family-migrations/).

The main config is read via GET /api/config and written via PUT /api/config. If you have set an editor password, both endpoints require an hs-session cookie: PUT accepts nothing else, and GET also accepts a display bearer token. With no password set, authentication is off and both are open on your local network.

Close the editor tab before hand-editing data/config.json. The editor loads the whole config into memory when the page opens and writes the whole file back when you save, so any change you made on disk in between is silently overwritten. PUT /api/config works the same way; it replaces the file wholesale rather than merging your changes into it.

API Keys & Credentials

API keys and credentials are managed through the editor UI under Settings > API keys and stored server-side in data/secrets.json via the /api/secrets endpoint. They are not stored in .env.local or in the config file.

Supported secret keys:

KeyUsed By
openweathermap_keyWeather (OpenWeatherMap provider), Air Quality
weatherapi_keyWeather (WeatherAPI provider)
pirateweather_keyWeather (Pirate Weather provider)
metoffice_keyWeather (Met Office provider)
unsplash_access_keyBackground rotation (Unsplash)
todoist_tokenTodoist module
google_maps_keyTraffic module (Google Routes)
tomtom_keyTraffic module (TomTom)
google_client_idGoogle Calendar, only with your own Google app (TVs and Limited Input devices client)
google_client_secretGoogle Calendar, only with your own Google app
google_web_client_idGoogle Photos import, only with your own Google app (web application client)
google_web_client_secretGoogle Photos import, only with your own Google app
microsoft_client_idOneDrive photos in the Photo Slideshow and Full-Screen Photo Viewer (the Application ID of your own Microsoft app)
nasa_api_keyBackground rotation (NASA APOD)
immich_urlImmich server URL (e.g. http://192.168.1.50:2283)
immich_api_keyImmich API key (Account Settings → API Keys)

Environment variables

A few behaviors are set in the Home Screens service's environment rather than in the editor. Add them with sudo systemctl edit home-screens and then restart the service (see Networking), because upgrades rewrite the service file itself. PORT is the exception.

VariableEffect
PORTThe port the server listens on. Set it in data/port.conf, not with an override: setup-system writes it into the service file from there and the kiosk browser reads the same file, so an override alone leaves the screen on the old port. See Networking
HS_GOOGLE_HOSTED=0Turns off Home Screens' own Google apps, so Google Calendar and Google Photos only sign in with a Google app you saved under Settings > API keys
HS_DISABLE_AUTO_UPDATE=1Never installs updates on a schedule, whatever Install updates automatically says
HS_DISABLE_TELEMETRY=1Never sends the anonymous usage report, whatever the setting says
HS_TRUSTED_PROXIESComma-separated proxy addresses whose forwarded client address is believed; see Restrict access by IP
HS_ALLOWED_ORIGINSOther sites allowed to send changes to the API; see Requests from other websites
HOME_SCREENS_DIRWhere an install that is not under /opt/home-screens/current lives, so updates and the data folder are found

Schema

The configuration has the following structure:

  • ScreenConfiguration contains a version number, a single GlobalSettings object, zero or more Screen objects, and zero or more Profile objects.
  • Each Screen contains zero or more ModuleInstance objects.
  • Each ModuleInstance has a ModuleStyle object, an optional ModuleSchedule, and a module-specific config object.
  • Each Profile has an optional ModuleSchedule for auto-activation.

Top Level

FieldTypeDefaultDescription
versionnumber16Config schema version. The app updates it when it upgrades an older file; never edit it by hand
settingsGlobalSettingssee GlobalSettingsSettings shared by every display (see GlobalSettings)
screensScreen[]one entryThe screens shown in single-display mode. Once displays is set, each display owns its own screens instead
profiles?Profile[]unsetNamed groups of screens in single-display mode (see Profile)
rules?DisplayRule[]unsetDisplay rules for legacy single-display mode (multi-display rules live on each DisplayNode).
displays?DisplayNode[]unsetThe multi-display registry (see DisplayNode). Omitted or empty means single-display mode

The displays field is opt-in. When it is undefined or empty, Home Screens runs in single-display mode and renders screens directly, this is the default for fresh installs and the unchanged behavior for any existing config that predates the multi-display feature. When displays is populated, each entry has its own owned screens, dimensions, and rotation; see DisplayNode below and the Multi-display guide for the full multi-display flow.

GlobalSettings

FieldTypeDefaultDescription
rotationIntervalMsnumber30000How long each screen shows before the next one, in milliseconds
displayWidthnumber1080Canvas width in pixels
displayHeightnumber1920Canvas height in pixels
displayTransform?string"90"Screen rotation: normal, 90, 180, or 270. 90 and 270 turn the screen to portrait
touchMatrix?number[]unsetSix numbers that line touch up with the picture on the hub's own screen. Leave unset and touch follows the screen rotation; [1, 0, 0, 0, 1, 0] leaves touch as it is
latitudenumber0Household latitude
longitudenumber0Household longitude
locationName?stringunsetName of the location, as shown in settings
timezone?stringunsetIANA timezone the displays keep time in (e.g. America/Chicago)
googleApp?string"own"Which Google app signs the household in when it has saved its own: own (its own app) or hosted (Home Screens' app, keeping the saved details for later). Without saved details, Home Screens' app is used either way
weatherWeatherSettingssee WeatherSettingsWeather source, location, and units (see WeatherSettings)
calendarCalendarSettingssee CalendarSettingsCalendar sources and options (see CalendarSettings)
sleep?SleepSettingsunsetDimming and sleep (see SleepSettings)
screensaver?ScreensaverSettingsunsetWhat a dimmed display shows (see ScreensaverSettings)
cursorHideSeconds?number3Seconds without mouse movement before the pointer hides
activeProfile?stringunsetID of the profile used whenever no scheduled profile matches
transitionEffect?stringunsetHow one screen changes to the next (see TransitionEffect). One of fade, slide, slide-up, zoom, flip, blur, crossfade, none
transitionDuration?number0.6Length of the screen transition in seconds
updateChannel?string"stable"Which builds the update check offers: stable, rc, beta, or nightly. Each channel also sees everything more stable than itself
advancedMode?booleanfalseShow the developer settings: the Test builds choice, build details, and the Plugins > Developer tab
alerts?AlertSettingsunsetAlert overlay (see AlertSettings)
telemetryEnabled?booleantrueSend anonymous usage statistics
fullscreenTheme?stringunsetTheme for every full-screen module that does not set its own: linen, paper, mist, sandstone, vellum, or bloom (light), charcoal, midnight, slate, aurora, obsidian, or horizon (dark)
pauseEnabled?booleantrueDouble-tap the active pagination dot to pause screen rotation
pauseTimeoutSeconds?number300Seconds before a paused display starts rotating again on its own; 0 = never
swipeEnabled?booleantrueFlick left or right on a touchscreen to change screens
setupHintEnabled?booleantrueShow a faint note with the editor address on a display that has no screens to show. Turn it off to keep a deliberately blank display blank
showRotationProgress?booleantrueDraw a thin line under the active pagination dot that fills while the screen is showing
showPaginationDots?booleantrueShow the row of screen dots at the bottom of the display
backupReminder?BackupReminderSettingsunsetBackup reminder (see BackupReminderSettings)
updateNotification?UpdateNotificationSettingsunsetNew-release banner (see UpdateNotificationSettings)
autoUpdate?AutoUpdateSettingsunsetInstall updates by itself at a set time (see AutoUpdateSettings). Hub only
locale?string"en-US"Language for every display, as a BCP-47 tag (e.g. en-US, de-DE). Also sets date and number formatting unless formattingLocale overrides it
formattingLocale?stringunsetA BCP-47 tag that changes only date and number formatting, leaving the language alone. Unset follows locale
timeFormat?stringunsetHousehold 12/24-hour preference: 12h or 24h. Every module that shows a time follows it; clocks do when their hourFormat is inherit, and the meal planner unless its own time format is set. Unset follows the usual clock of the formatting language

SleepSettings

FieldTypeDefaultDescription
enabledbooleanunsetTurn dimming and sleep on
idleDimEnabled?booleantrueDim, then sleep, after a stretch with no touches. The schedules below run either way
dimAfterMinutesnumberunsetMinutes without a touch before the display dims
sleepAfterMinutesnumberunsetMinutes without a touch before the display sleeps
dimBrightnessnumberunsetBrightness while dimmed, 0 to 100
dimSchedule?{ startTime: string; endTime: string; }unsetA daily window to stay dimmed: startTime and endTime as HH:MM
schedule?{ startTime: string; endTime: string; }unsetA daily window to sleep: startTime and endTime as HH:MM
wakeHoldMinutes?number5How long a wake-up during a scheduled dim or sleep window keeps the display on, in minutes; 0 = not at all
panelPowerOff?booleanfalseAlso switch the screen's power off while asleep (Raspberry Pi kiosks only)

ScreensaverSettings

FieldTypeDescription
modestringWhat a dimmed display shows: clock, blank, or off

AlertSettings

FieldTypeDefaultDescription
enabledbooleanunsetShow alerts sent to the display
positionstringunsetWhere alerts appear: top or bottom
maxVisiblenumberunsetMost alerts shown at once
defaultDurationnumberunsetHow long an alert stays up, in milliseconds; 0 = each alert type's own default
scale?number1Size of the alerts, 0.75 to 2

BackupReminderSettings

FieldTypeDefaultDescription
enabledbooleantrueRemind you when a backup is overdue
intervalDaysnumber7Days between reminders

UpdateNotificationSettings

FieldTypeDescription
enabledbooleanShow a banner when a new release is available

WeatherSettings

FieldTypeDefaultDescription
providerstring"open-meteo"Weather data source. One of openweathermap, weatherapi, pirateweather, noaa, open-meteo, yr, smhi, metoffice, envcanada
latitudenumber0Latitude the forecast is fetched for
longitudenumber0Longitude the forecast is fetched for
unitsstring"imperial"metric or imperial
radarServerUrl?stringunsetRain map radar server: any LibreWXR server. Blank uses the public one

CalendarSettings

FieldTypeDefaultDescription
googleCalendarIdstring""A single Google calendar ID, from before several could be picked; googleCalendarIds replaced it
googleCalendarIdsstring[][]Google calendars to show
icalSourcesICalSource[][]iCal (ICS) feeds (see ICalSource)
icloudSources?ICloudSource[]unsetiCloud calendars picked from connected accounts (see ICloudSource)
people?CalendarPerson[]unsetAn older list of people and their calendars, kept only until it has been moved onto the family list
personSources?Record<string, string[]>unsetWhose calendars are whose: a family member ID to the calendar source IDs that belong to them. A calendar nobody claims is shared by the household
daysAheadnumber7How many days ahead to fetch events
holidayCountry?stringunsetCountry for public holidays, as a two-letter ISO code (e.g. US)
hideDeclined?booleanunsetGoogle only: skip events the signed-in account declined

The family grid and free time views join personSources against data/family.json. Manage people under Settings > Family and their calendars under Settings > Calendar > Whose calendars? A calendar that no person claims is shared by the whole household. Legacy people records are preserved by the schema upgrade until the coordinated family migration folds them safely.

ICalSource

FieldTypeDescription
idstringUnique ID
type"ical"Always ical
namestringName shown in the editor and in calendar legends
urlstringFeed address
colorstringColor for this calendar's events
enabledbooleanWhether the feed is shown
homeNetwork?booleanLet the hub fetch this feed from an address on the home network, for a self-hosted calendar server

ICloudSource

FieldTypeDescription
idstringUnique ID
accountIdstringThe connected iCloud account, from data/icloud-accounts.json. Passwords never live in the config
kindstringcalendar for a calendar, birthdays for your contacts' birthdays
urlstringThe calendar's address; empty for birthdays
namestringCalendar name
colorstringCalendar color, kept from iCloud
enabledbooleanWhether the calendar is shown

Account credentials (Apple ID + app-specific password) are not stored in the config file, they live in data/icloud-accounts.json and are referenced by accountId.

TransitionEffect

One of fade, slide, slide-up, zoom, flip, blur, crossfade, none.

Screen

FieldTypeDefaultDescription
idstringunsetUnique ID
namestringunsetName shown in the editor
enabled?booleantrueWhether the screen is in the rotation
backgroundImagestringunsetBackground image path; empty for none
backgroundRotation?BackgroundRotationunsetRotating background images (see BackgroundRotation)
shade?BackgroundShadeunsetDim/vignette overlay drawn over the background (see BackgroundShade)
modulesModuleInstance[]unsetThe modules on this screen (see ModuleInstance)
rotationDurationMs?numberunsetHow long this screen shows, in milliseconds. Unset uses the display's rotation interval; 0 keeps the screen up until someone moves on
schedule?ModuleScheduleunsetOnly put this screen in the rotation during these days and times (see ModuleSchedule). When no scheduled screen matches, every enabled screen shows

BackgroundRotation

FieldTypeDescription
enabledbooleanTurn rotation on
source?stringWhere the images come from. One of unsplash, nasa-apod, immich, icloud
querystringUnsplash search words; other sources ignore it
intervalMinutesnumberMinutes between images
immichAlbumId?stringImmich album to use
immichPersonId?stringImmich person (face) to use
immichFavoritesOnly?booleanOnly use Immich favorites
icloudAlbumUrl?stringPublic iCloud shared album link, or just its token

BackgroundShade

FieldTypeDescription
enabledbooleanTurn the shade on
stylestringHow the shade is shaped over the background. One of even, topBottom, edges, both
strengthnumberOverlay intensity, 0-100
colorstringShade color (hex or rgb())

ModuleInstance

FieldTypeDefaultDescription
idstringunsetUnique ID
enabled?booleantrueSet to false to hide the module on the display without deleting it; the editor still shows it, dimmed
typestringunsetModule type, such as clock, or plugin:<name> for an add-on. One of clock, calendar, weather, countdown, dad-joke, text, image, video, quote, todo, sticky-note, greeting, news, fullscreen-news, stock-ticker, crypto, word-of-day, history, moon-phase, sunrise-sunset, photo-slideshow, qr-code, year-progress, traffic, sports, air-quality, todoist, rain-map, multi-month, garbage-day, standings, affirmations, date, display-control, meal-planner, iframe, icon, shape, chore-chart, timetable, fullscreen-calendar, fullscreen-chore-chart, fullscreen-meal-planner, fullscreen-photo, fullscreen-weather
positionModulePositionunsetTop-left corner in pixels: { x, y }
sizeModuleSizeunsetWidth and height in pixels: { w, h }
zIndexnumberunsetStacking order; higher draws on top
configRecord<string, unknown>unsetThe module's own settings, listed module by module in the Module Reference
styleModuleStyleunsetCard styling (see ModuleStyle)
schedule?ModuleScheduleunsetOnly show the module during these days and times (see ModuleSchedule)
visibility?ModuleVisibilityunsetOnly show the module while live values meet these conditions (see ModuleVisibility)
backgroundProvider?booleanunsetNever draw the module; run it in the background instead, so the values it publishes keep updating while other screens show

The three visibility gates (enabled, schedule, visibility) are AND-combined: a module renders only when it is enabled, its schedule window matches, and its visibility conditions are met.

ModuleSchedule

Controls when a module (or profile) is active based on day of week and time window.

FieldTypeDescription
daysOfWeek?number[]Days the window opens, 0 = Sunday through 6 = Saturday. Omit for every day
startTime?stringStart, as HH:MM. Omit to start at midnight
endTime?stringEnd, as HH:MM. Omit to end at midnight. An end earlier than the start closes the next morning; an end equal to the start runs a full 24 hours
endDayOffset?numberDays after the start day that the window closes, 0 to 6, for one long stretch such as Monday 08:00 until Thursday 20:00
invert?booleanHide during the window instead of showing

ModuleVisibility

Shows or hides a module based on values published to the shared state bus. Most values come from plugins, via the SDK's publishState. Marking a plugin instance backgroundProvider keeps it publishing across screen rotation; the flag has no state-publishing effect on built-in modules, which do not publish anything of their own. Home Screens itself publishes two small sets of built-in values that need no module on screen and survive rotation. calendar.* comes from the display's shared calendar fetch (next event, how many events today, whether something is on right now) whenever a calendar source is configured. weather.* comes from the default weather provider whenever a location is set:

  • weather.condition: clear, clouds, rain, drizzle, snow, thunderstorm, fog or wind, read from the current hour
  • weather.temperature: the current reading in the household's unit, whole degrees
  • weather.precip_chance_now: chance of rain or snow this hour, 0 to 100
  • weather.precip_chance_next_6h: the highest chance in the next six hours
  • weather.precip_chance_today: the highest chance for the rest of today, on the display's calendar day, including the forecast's own figure for the day
  • weather.alert_active: true while a weather warning that has not expired is in force, otherwise false
  • weather.alert_severity: minor, moderate, severe or extreme for the most serious active warning, blank when there is none
  • weather.alert_title: that warning's name, such as "Winter Storm Warning", blank when there is none

A chance key is blank when the provider does not report one, which a numeric condition treats as not met. Only Pirate Weather and NOAA send warnings, so the three warning keys appear in the key picker only while the household's default weather provider is one of those; on every other provider they are still published, with weather.alert_active at false, so a condition saved earlier keeps evaluating. Conditions follow Home Assistant-style semantics.

FieldTypeDefaultDescription
conditionsVisibilityCondition[]unsetConditions that must all hold for the module to show (see VisibilityCondition)
whenUnknown?string"hide"What to do while a value the conditions read has not been published yet: hide or show

VisibilityCondition

Each condition is one of these shapes, told apart by its kind:

kind: "state": Compares a published value as text

FieldTypeDescription
sourceKeystringKey of the published value
equals?string | string[]Holds when the value is this text, or one of these
notEquals?string | string[]Holds when the value is not this text, nor any of these

kind: "numeric": Compares a published value as a number

FieldTypeDescription
sourceKeystringKey of the published value
above?numberHolds when the value is greater than this
aboveInclusive?booleanCount a value equal to above as holding
below?numberHolds when the value is less than this
belowInclusive?booleanCount a value equal to below as holding

kind: "time": A day-and-time window on the display's clock, with the same fields as ModuleSchedule. It reads no published value, so it never waits on one

FieldTypeDescription
daysOfWeek?number[]Days the window opens, 0 = Sunday through 6 = Saturday. Omit for every day
startTime?stringStart, as HH:MM. Omit to start at midnight
endTime?stringEnd, as HH:MM. Omit to end at midnight
endDayOffset?numberDays after the start day that the window closes, as in ModuleSchedule

kind: "and": Holds when every condition inside it holds

FieldTypeDescription
conditionsVisibilityCondition[]The conditions in the group

kind: "or": Holds when any condition inside it holds

FieldTypeDescription
conditionsVisibilityCondition[]The conditions in the group

kind: "not": Holds when none of the conditions inside it hold

FieldTypeDescription
conditionsVisibilityCondition[]The conditions in the group

sourceKey references a published state key (plugin keys are prefixed plugin:<id>:, built-in ones are not). Conditions are edited visually in the module's Conditions section in the editor; the key picker lists the built-in values first, under Built-in, followed by the keys plugins declare in their manifest's providesState field or compute from their config via a deriveProvidedKeys export. Check the Or equal to box next to a numeric bound to make it inclusive. See the Plugins guide for the publishing side.

Save-time limits: at most 32 conditions per module (leaves and groups combined) and 5 levels of and / or / not nesting, and a group condition must have at least one child. Exceeding any of these makes PUT /api/config fail with a 400 rather than saving.

Profile

Named groups of screens that can be activated manually or on a schedule.

FieldTypeDescription
idstringUnique ID
namestringName shown in the editor (e.g. Morning)
screenIdsstring[]IDs of the screens this profile shows
schedule?ModuleScheduleWhen the profile turns itself on (see ModuleSchedule)

Profiles support overnight windows (e.g. 23:00–06:00). Scheduled profiles take precedence: at each tick the first profile in list order whose schedule matches, and that still resolves to at least one screen, wins. settings.activeProfile is the fallback used when no scheduled profile matches. If neither produces screens, all screens are shown.

DisplayRule

A condition → action rule owned by a display. Rules reuse the VisibilityCondition tree and evaluator unchanged, but are edge-triggered: a rule fires only on the false→true transition of its conditions, never while they merely stay true, so a reboot or a restarting state producer never slams the display onto an alert screen for a condition that has been true for hours. Rules live under Settings > Automation > Rules and are per-display in multi-display setups.

FieldTypeDefaultDescription
idstringunsetUnique ID
namestringunsetName shown in the editor, e.g. Doorbell → front camera
enabled?booleantrueSet to false to switch the rule off
whenVisibilityCondition[]unsetConditions that must all become true for the rule to fire (see VisibilityCondition)
actionRuleActionunsetWhat happens when it fires (see RuleAction)
cooldownSeconds?number0Seconds after firing before the rule can fire again

When multiple rules could fire at once, the first one in list order wins; reorder rules by dragging their cards. In multi-display setups, a rule can be copied to another display, since screens are per-display, a copied showScreen action arrives with its target screen cleared, ready to point at a screen on the new display.

Save-time limit: at most 64 rules per display. The when tree obeys the same condition and nesting limits as ModuleVisibility above.

RuleAction

kind: "showScreen": Show one screen

FieldTypeDescription
screenIdstringThe screen to show, from the rule's own display
modestringwhile keeps the screen up as long as the conditions hold (at least 5 seconds); for shows it for seconds, then rotation carries on
seconds?numberHow long to show the screen; required when mode is for

kind: "wake": Wake the display; nothing happens if it is already awake

kind: "sleep": Put the display to sleep, like the remote sleep command, and end any screen a rule is holding

DisplayNode (multi-display)

A named display device. Each display owns its own list of screens, designed at its own resolution and orientation. Used in multi-display deployments where one server drives multiple Pi displays. See the Multi-display guide for the install and adoption flow.

FieldTypeDescription
idstringURL-safe ID, which is also the display's address: /display/<id>
namestringName shown in the editor
screensScreen[]This display's own screens, laid out at its resolution
displayWidth?numberCanvas width in pixels, instead of the shared one
displayHeight?numberCanvas height in pixels, instead of the shared one
displayTransform?stringScreen rotation for this display: normal, 90, 180, or 270
touchMatrix?number[]Six numbers that line touch up with the picture, instead of following the screen rotation
profiles?Profile[]This display's own profiles. Their screenIds point at this display's screens
activeProfile?stringProfile used when no scheduled one matches; unset uses the shared activeProfile
settings?DisplayNodeSettingsSettings this display overrides (see DisplayNodeSettings)
rules?DisplayRule[]This display's own rules (see DisplayRule). There is no shared set in multi-display mode

Like screens, the profiles field is owned by the display: owned profile screenIds reference the display's own screens, not the global pool. When the first additional display is added to a single-display install, the existing config.profiles and config.settings.activeProfile migrate onto the auto-created main display alongside its screens; subsequent displays start with profiles: [] so they build fresh against their own screens. In multi-display mode profiles are always per-display, there is no "shared pool" escape hatch, because a pool profile's screenIds would silently diverge from each display's owned screens as soon as either one is edited.

DisplayNodeSettings is a subset of GlobalSettings that can be overridden per display. Nested objects (sleep, screensaver, alerts) are full-replacement, not deep-merged, override the whole object or omit it:

FieldTypeDescription
displayWidth?numberCanvas width in pixels
displayHeight?numberCanvas height in pixels
displayTransform?stringScreen rotation. One of normal, 90, 180, 270
rotationIntervalMs?numberHow long each screen shows, in milliseconds
transitionEffect?stringHow one screen changes to the next. One of fade, slide, slide-up, zoom, flip, blur, crossfade, none
transitionDuration?numberLength of the transition in seconds
sleep?SleepSettingsDimming and sleep, replacing the shared object as a whole
screensaver?ScreensaverSettingsWhat a dimmed display shows, replacing the shared object as a whole
fullscreenTheme?stringTheme for full-screen modules
cursorHideSeconds?numberSeconds before the pointer hides
pauseEnabled?booleanDouble-tap the active dot to pause rotation
pauseTimeoutSeconds?numberSeconds before a pause ends on its own
swipeEnabled?booleanSwipe to change screens
setupHintEnabled?booleanShow the setup note on a display with no screens
showRotationProgress?booleanShow the rotation progress line
showPaginationDots?booleanShow the row of screen dots
alerts?AlertSettingsAlert overlay, replacing the shared object as a whole

Per-display location overrides (latitude, longitude, locationName, timezone) are intentionally not available: the weather, air-quality, and calendar API routes read location via readConfig() directly rather than through filterConfigForDisplay, so a per-display override would only affect client rendering while the upstream fetch still used the hub's coordinates.

Per-display dimension fields (top-level on the DisplayNode) override the equivalents nested inside settings. Per-display settings override the global settings on a per-key basis. Rotation is authoritative for canvas orientation: the hub sorts the (width, height) pair so the long edge points along the landscape axis when the rotation is normal/180 and along the portrait axis when it's 90/270.

Validation rules (enforced when the config is written):

RuleLimit
Display ID formatURL-safe slug, lowercase letters, digits, hyphens; must start with a letter or digit
Display ID length≤ 64 characters
Reserved IDsall is refused, because the command endpoints use it to mean every display
IDs must be uniqueYes
Maximum displays64 per config
Maximum screens per display256
DimensionsWhole numbers from 320 to 16384
Owned profile IDsMust be unique within the display's profiles list
Owned profile screenIdsMust reference the display's own screens (not the global pool)
activeProfile referencesWhen owned profiles are present, must be a member of profiles; otherwise must reference the global profile list

ModuleType

There are 45 built-in module types. Plugin modules use the plugin:<name> format.

One of clock, calendar, weather, countdown, dad-joke, text, image, video, quote, todo, sticky-note, greeting, news, fullscreen-news, stock-ticker, crypto, word-of-day, history, moon-phase, sunrise-sunset, photo-slideshow, qr-code, year-progress, traffic, sports, air-quality, todoist, rain-map, multi-month, garbage-day, standings, affirmations, date, display-control, meal-planner, iframe, icon, shape, chore-chart, timetable, fullscreen-calendar, fullscreen-chore-chart, fullscreen-meal-planner, fullscreen-photo, fullscreen-weather.

ModuleStyle

FieldTypeDefaultDescription
opacitynumber1Module opacity, 0 to 1
borderRadiusnumber12Corner rounding in pixels
paddingnumber16Inner padding in pixels
backgroundColorstring"rgba(0, 0, 0, 0.4)"Card background, any CSS color (e.g. rgba(0,0,0,0.4))
textColorstring"#ffffff"Text color, any CSS color
fontFamilystring"inter"Font id from the built-in list below
fontSizenumber16Text size in pixels (the smallest size, on modules that fit their text to their box). Stands as it is while textScale is unset
textScale?numberunsetText size as a percent, 10 to 450, of what the module shows on its own. Unset leaves fontSize as it is
fontWeight?numberunsetOne weight, 100 to 900, for all of the module's text. Unset keeps each module's own weights
title?stringunsetCentered title strip above the module content; empty or unset = no strip
titleFontSize?numberunsetTitle strip text size in pixels; unset uses fontSize
backdropBlurnumber12Blur behind the card, in pixels
borderWidthnumber1Border width in pixels
borderColorstring"rgba(255, 255, 255, 0.15)"Border color, any CSS color
shadowSizenumber8Shadow size in pixels

title and titleFontSize only apply to modules that render the standard card frame. Plugin modules and Display Control draw their content without the card, so a title strip can never appear on them, the editor hides the Card Title fields for those modules, and both keys are dropped from a plugin manifest's defaultStyle when a module is placed.

fontFamily stores a font registry id, not a raw CSS stack. The available ids are inter, roboto, poppins, system-ui, playfair, lora, dm-serif, georgia, jetbrains, mono, bebas, caveat, and pacifico. The fonts themselves are bundled at build time, so only these ids are guaranteed to load. A raw CSS stack is still accepted for backward compatibility, but anything the registry does not recognize is passed to the browser verbatim and will fall back to a system font.

Module Configs

Each ModuleInstance.config object holds the fields for its module type. Those fields, with their defaults and allowed values, are documented one table per module in the Module Reference: that page is the source of truth for module options, and the stored JSON matches it exactly.

Shared data files

Several features deliberately keep their data outside config.json: the four below, and the timetables, routines and running timer listed in Data files. The per-module config holds display options only; the data itself lives in a shared file so every module instance stays in sync, and so a save from the editor can never clobber something changed from /remote or tapped on a display.

data/meals.json

The meal library, weekly plan, grocery check-offs, and household-wide planning settings, written atomically via /api/meals/data. Settings live here rather than on each module so /remote and every meal-planner instance agree.

FieldTypeDescription
savedMealsSavedMeal[]The meal library (see SavedMeal below)
planPlannedMeal[]Planned meals, one entry per day and slot (see PlannedMeal)
groceryCheckedGroceryCheckedIngredient names checked off each week's grocery list, keyed by the date that week starts on. The list itself is built from the week's planned meals and is not stored
settingsMealSettingsHousehold meal settings (see MealSettings)
timeFormatLegacyStripped?booleanSet once the app has cleared an old time-format default from settings; leave it alone

SavedMeal

Each entry in savedMeals:

FieldTypeDescription
idstringUnique ID
namestringMeal name
emoji?stringEmoji shown with the meal
category?stringKind of dish. One of main, side, dessert, drink, snack
tags?string[]Free-form tags
prepTime?numberPrep time in minutes
cookTime?numberCook time in minutes
difficulty?stringHow hard it is to make. One of easy, medium, hard
servings?numberHow many people it serves
ingredients?MealIngredient[]Ingredients, for the grocery list (see MealIngredient)
recipeUrl?stringLink to the recipe, which a display can open
notes?stringNotes
rating?numberRating, 1 to 5
isFavorite?booleanMarked as a favorite

Each of its ingredients:

FieldTypeDescription
namestringIngredient
amount?stringAmount, as text (e.g. 2 cups)
category?stringGrocery aisle. One of produce, dairy, meat, seafood, bakery, pantry, frozen, beverages, other

MealSettings is edited from the /remote Meals tab so every meal module on every display stays consistent:

FieldTypeDefaultDescription
enabledSlotsMealSlotType[]["breakfast","lunch","dinner"]Which meals of the day are planned
weekStartDaystring"sunday"First day of the planning week. One of sunday, monday
defaultSlotTimesPartial<Record<MealSlotType, string>>{}Default serving time per slot, as 24-hour HH:MM. A planned meal's own time wins
timeFormat?stringunset12h or 24h for meal times. Unset follows the household time format

timeFormat is optional: when omitted (the default), meal times follow the household GlobalSettings.timeFormat; an explicit '12h'/'24h' wins everywhere meals are shown. Versions before this setting existed wrote '12h' into every file whether or not the user had touched the picker, so on first read after upgrading, a stored '12h' is treated as never-configured and removed once (a timeFormatLegacyStripped marker in the file keeps a later deliberate '12h' pick from being stripped again). A stored '24h' was always deliberate and survives as an explicit override.

Each PlannedMeal uses an ISO date string to support multi-week planning:

FieldTypeDescription
datestringDay, as YYYY-MM-DD
slotstringWhich meal of the day. One of breakfast, lunch, dinner, snack
mealId?stringThe saved meal, by ID
customText?stringText instead of a saved meal (e.g. Eating out)
notes?stringNotes
time?stringServing time, as 24-hour HH:MM, instead of the slot's default for this one day

Old configs that used day: number (day-of-week index) are automatically migrated to ISO date format on first read.

data/family.json

The shared household roster, edited under Settings > Family or /remote > Settings > Family. /api/family returns its members and groups and a revision required for writes; groups are edited through /api/family/groups.

FieldTypeDescription
membersFamilyMember[]The household, in display order (see the member table below)
groups?FamilyGroup[]Named sets of members, used by the calendar's color key and people filter
migrated?booleanSet once older chore and calendar people have been folded into this list
aliasIds?Record<string, string>Older calendar person IDs, mapped to the member each one became

Each entry in members:

FieldTypeDescription
idstringUnique ID
namestringName
colorstringColor used for this person everywhere
emoji?stringEmoji shown with the name
createdAtstringWhen the person was added (ISO timestamp)
updatedAtstringWhen the person was last changed (ISO timestamp)

Chore identities survive migration and legacy restore by ID. Calendar identities are matched by ID or alias first, then by a unique normalized name. Existing rosters over 64 people and long legacy names are retained. The authoring API limits new additions to 64 people and new names to 40 characters.

data/chores.json

Chore definitions, served by /api/chores/data and edited from the /remote Chores tab. Family members are read separately from /api/family.

FieldTypeDescription
choresChoreDefinition[]Chore definitions (see ChoreDefinition)
settings?ChoreSettingsHousehold chore settings (see ChoreSettings). Missing = the defaults
FieldTypeDescription
idstringUnique ID
namestringChore name
emojistringEmoji shown with the chore
pointsnumberTickets earned for doing it; 0 or more (0 = no reward)
frequencystringHow often it comes around. One of daily, weekly, biweekly, once
daysOfWeeknumber[]Days it happens, 0 = Sunday through 6 = Saturday
specificDate?stringThe day, as YYYY-MM-DD, when frequency is once
timeOfDaystringWhen in the day it belongs. One of morning, afternoon, evening, anytime
assigneeIdsstring[]Family members it can go to
assigneeGroupIds?string[]Family groups it goes to, expanded to their members when the chore is resolved
rotationstringHow it is shared among the people it goes to: everyone every time (fixed), taking turns by day or by week, or by schedule, where each person and each group has its own days. One of fixed, rotate-daily, rotate-weekly, schedule
schedule?Record<string, number[]>With rotation set to schedule: each member ID to the days (0 to 6) they have the chore
groupSchedule?Record<string, number[]>With rotation set to schedule: each family group ID to the days (0 to 6) everyone in it has the chore
bonus?ChoreBonusSet on a bonus chore: an extra that only earns tickets and never counts against anyone

Completions live in a separate file, data/chore-completions.json, served by /api/chores:

FieldTypeDescription
completionsChoreCompletion[]Chores done, one entry per person per day (see ChoreCompletion). Entries older than 90 days are removed
grabs?ChoreGrab[]Up-for-grabs chores someone is holding (see ChoreGrab). Lapsed ones are ignored when read and dropped after 90 days
bonusResets?Record<string, string>When a grown-up last put each "when I put it back" bonus chore back, by chore ID, as an ISO timestamp

Each entry in completions:

FieldTypeDescription
choreIdstringThe chore
memberIdstringWho did it
datestringThe day it was done, as YYYY-MM-DD
status?"skipped"skipped marks a chore a grown-up set to "not today" for this person: it pays nothing and leaves every count. Omitted = done.
at?stringWhen it was marked, as an ISO timestamp. Missing on entries saved before it was recorded.
endedGrab?stringThe day of the person's own grab this finished, as YYYY-MM-DD. Un-ticking hands that grab back, with its own date, so a tick and an un-tick never restart how long it lasts; un-ticking a tick nobody had grabbed for (a grown-up's slip) hands nothing back.

data/todos.json

The family's shared to-do lists (see Lists). Each list has a name, a colour, an optional start-fresh repeat, and its items with done state, due day and who it is for. A To-Do module only stores which list it shows, so editing a screen never touches a list and the same list can sit on any number of screens. The phone, the wall and the editor all write through /api/todo/lists.

Display Resolution Presets

In the editor you pick a resolution and an orientation separately: four presets, labelled 720p HD, 1080p Full HD, 1440p QHD, and 4K UHD, each with a Portrait/Landscape toggle that swaps the two dimensions. That gives eight combinations:

PresetWidthHeight
Portrait 720p7201280
Portrait 1080p10801920
Portrait 1440p14402560
Portrait 4K21603840
Landscape 720p1280720
Landscape 1080p19201080
Landscape 1440p25601440
Landscape 4K38402160

Config Migrations

Config files include a version number. When the schema changes between releases, migrations in src/lib/migrations/ automatically transform older configs to the current format on load. The current schema version is 16.

Migration runs when the config is read and the result is written back to disk automatically, so version in data/config.json updates itself the first time newer code reads an older config. If a migration fails, the un-migrated config is returned as-is rather than falling back to defaults, so a bad upgrade can never quietly replace your setup with an empty one.

Do not hand-edit version. A config marked with a version newer than the running code is left alone; there is no downgrade path.

Validation CLI

Home Screens ships a standalone validator for data/config.json that you can run without starting the dev server. It checks the schema version, module types, screen and module structure, profile references, multi-display registry constraints, and settings bounds, then reports a typed list of diagnostics with colored output.

npm run config:check

A clean config exits with status 0 and a "Config is valid" summary. Any errors, unknown module types, duplicate screen IDs, profile references to non-existent screens, out-of-range display dimensions, etc., exit with a non-zero status and a list of diagnostic entries, making the CLI safe to wire into a pre-commit hook or CI step on a server that mounts data/. The same validation rules are exposed programmatically from src/lib/validate-config.ts if you want to reuse them from your own tooling.

Backup & Restore

  • Save a backup (under Save a copy in Settings > Backups & data) in the editor, or Backup All Data in the remote's Settings sheet, downloads a backup as JSON. The bundle contains your config, family, chores, chore completions, rewards, meals, routines, to-do lists and school timetables, plus your icons when Include your icons is on.
  • Import replaces everything the uploaded file carries (available in both the editor and the remote); a section the file leaves out, such as the icons, is kept as it is
  • A configurable backup reminder shows a toast in the editor and a banner on the remote when you haven't backed up recently (Settings > Backups & data)

Backing up your keys

By default a backup carries no keys at all. This part needs an editor password (Settings > Security): without one, anything on your network can ask the hub for your keys, and there is no way for it to tell you apart from anyone else. Until you set one, the checkbox below stays switched off.

In the editor, Settings > Backups & data > Save a copy has two checkboxes:

  • Include my keys and connected accounts adds your weather and map keys, your Google, iCloud, Immich and OneDrive sign-ins, your plugin logins, and your editor password to the file. Both boxes start unticked every time you open the page, so an accidental tick never becomes your standing default.
  • Protect them with a password locks that part of the file. You choose the password, and you need it again to put the keys back.

A backup that carries keys is named home-screens-backup-with-keys-<date>.json, so you can tell it apart in a downloads folder.

There is no way to recover the password. If you lose it you can still restore the backup, choose Restore without my keys at the prompt, and everything except the keys comes back. Losing the password costs you the keys, not your setup.

Restoring a backup that includes your editor password signs you out, because the password on this device becomes the one from the backup. Sign in again with that password. If the backup also had network address rules turned on and this device isn't on the restored list, the rules are left switched off so you can't lock yourself out; turn them back on in Settings once you've added this device.

The remote control's Settings sheet has no password prompt. Restoring a key-carrying backup there restores everything except the keys and tells you so, finish that part in the editor.

Keep a key-carrying backup private. Without the password option, anyone who opens the file can use your accounts. With it, the keys are sealed and only your password opens them.

Plugin bundles themselves are still not in the backup, so a restored Pi comes up with its plugins missing (their saved logins return once you reinstall them). For a genuinely complete copy, take the whole data/ directory instead. Copying data/config.json on its own is narrower still: it captures none of your chores, completions, meals, or rewards.

Example

{
  "version": 16,
  "settings": {
    "rotationIntervalMs": 30000,
    "displayWidth": 1080,
    "displayHeight": 1920,
    "latitude": 40.0150,
    "longitude": -105.2705,
    "timezone": "America/Denver",
    "weather": {
      "provider": "pirateweather",
      "latitude": 40.0150,
      "longitude": -105.2705,
      "units": "imperial"
    },
    "calendar": {
      "googleCalendarId": "",
      "googleCalendarIds": ["primary"],
      "icalSources": [],
      "daysAhead": 7
    }
  },
  "screens": [
    {
      "id": "abc-123",
      "name": "Main",
      "backgroundImage": "/backgrounds/sunset.jpg",
      "modules": [
        {
          "id": "mod-1",
          "type": "clock",
          "position": { "x": 20, "y": 40 },
          "size": { "w": 1040, "h": 220 },
          "zIndex": 1,
          "config": {
            "view": "classic",
            "format24h": false,
            "showSeconds": true,
            "showDate": true
          },
          "style": {
            "opacity": 1,
            "borderRadius": 12,
            "padding": 16,
            "backgroundColor": "rgba(0,0,0,0.4)",
            "textColor": "#ffffff",
            "fontFamily": "inter",
            "fontSize": 16,
            "backdropBlur": 12,
            "borderWidth": 1,
            "borderColor": "rgba(255,255,255,0.15)",
            "shadowSize": 8
          }
        }
      ]
    }
  ]
}
Previous
Networking