Skip to content

feat: add Home Screen widgets, SSE sitemap updates, and wide-display grid#1028

Open
timbms wants to merge 112 commits into
developfrom
feature/widgets-with-item-display
Open

feat: add Home Screen widgets, SSE sitemap updates, and wide-display grid#1028
timbms wants to merge 112 commits into
developfrom
feature/widgets-with-item-display

Conversation

@timbms

@timbms timbms commented Dec 20, 2025

Copy link
Copy Markdown
Contributor

Overview

Four features landing together:

  1. Home Screen widget extension — interactive Switch widgets and a read-only Sensor widget backed by real-time SSE updates
  2. SSE migration for sitemap pages — replaces the long-poll loop with a Server-Sent Events stream; widget state changes are applied in-place, reducing UI churn and data usage by ~6×
  3. Wide-display sitemap grid — two-column adaptive grid on iPad; media rows (image, video, chart, webview, map) span full width, matching BasicUI behaviour
  4. CarPlay support — sitemap-based favorites scene with real-time SSE updates; uses CPGridTemplate with openHAB SF symbol icons

Raises minimum deployment targets to iOS 18 / watchOS 11 / macOS 15 (required for interactive widget APIs and AsyncSequence.Failure / SE-0421).


Widget extension (openHABWidget)

Switch widgets

Three sizes backed by separate WidgetConfigurationIntents:

Size Items Configuration intent
Small 1 Switch item SwitchSmallConfigurationAppIntent
Medium 2 Switch items SwitchMediumConfigurationAppIntent
Large 4 Switch items SwitchLargeConfigurationAppIntent

Each widget lets the user select a Home and then one or more Switch items. Tapping a switch sends the toggle command via SetSwitchItemIntent.

Sensor widget

Displays the current state of a Number or String item (e.g. temperature, humidity). Available in small and medium sizes. Refreshes every 5 minutes; also updated live via SSE.

Real-time widget updates

WidgetItemRegistry (new, OpenHABCore) — actor that tracks which items have active widgets using App Group UserDefaults (group.org.openhab.app), shared between the widget extension and the main app without IPC.

WidgetItemMonitor (new, main app) — started from AppDelegate on launch. Subscribes to ItemEventStream SSE events and calls WidgetCenter.shared.reloadTimelines(ofKind:) for all widget kinds when a tracked item's state changes. Stale registry entries are cleaned up each time the app becomes active.


Sitemap SSE migration

Network savings — measured with Proxyman over a ~30 s session with periodic state changes:

                         SSE session    Long-poll session
─────────────────────────────────────────────────────────
  /rest/sitemaps/…/…          44 KB          308 KB
  /rest/sitemaps/events/{id}   4 KB            —
─────────────────────────────────────────────────────────
  Total                        48 KB          308 KB   ← 6× less

Per state change, each long-poll round-trip returns ~38 KB of full page JSON; a typical SSE widget event is 100–300 bytes — roughly 100–300× smaller.

SitemapEventStream (new, OpenHABCore) — actor-isolated SSE connection manager: subscribes to the server's sitemap event endpoint, broadcasts SitemapEventMessage values (.alive, .sitemapChanged, .widget), reconnects with exponential backoff, and includes a 30 s watchdog to detect silently dead connections.

OpenAPIServiceProtocol — adds openHABSitemapWidgetEvents (returning any AsyncSequence<SitemapEventMessage, any Error> & Sendable) and openHABEvents. SSE event parsing and 404 → SitemapSseError.unsupported mapping are encapsulated inside OpenAPIService.

SitemapPageViewModel — after initial fetch, checks shouldUseSSE(); if true, hands off to startSSE and returns from the polling loop. applySseWidgetEvent applies widget delta events in-place via OpenHABWidget.apply(event:); falls back to full page reload when a widget is not found or its description changed. startLongPollingFallback sets ssePreferred = false and restarts polling.

OpenHABWidget.apply(event:) (new, OpenHABCore) — recursive in-place widget update from an OpenHABSitemapWidgetEvent. Returns .applied, .notFound, or .requiresPageReload.


Wide-display sitemap grid

On iPad (and any wide horizontal size class), sitemap pages now use a two-column adaptive grid via SitemapGridLayout. Media widgets — image, video, chart, webview, and map — are classified as full-width cells and span both columns, matching BasicUI.

SitemapGridLayout (new) — groups SitemapRowInput items into sections and row groups; classifies cells as half-width or full-width based on widget type.

SitemapPresentationPolicy (new) — decides between list and grid presentation based on horizontal size class; drives SitemapPageView layout switching.

EmbeddingRowInputView — media rows now use standard regularInsets (16 pt leading/trailing) instead of zero-inset edge-to-edge bleed.

Image rowsKFImage (static images, charts) gets frame(maxWidth: .infinity) so landscape images fill the available width. KFAnimatedImage (GIFs) uses AnimatedGIFFrameModifier which computes an exact frame from the GIF's natural aspect ratio via onGeometryChange (no GeometryReader).

Video rows — fixed frame(height: 200) removed; height is now driven by aspectRatio(contentMode: .fit) + frame(maxWidth: .infinity).


CarPlay support

CarPlay presents a sitemap-based favorites scene configurable in Settings → CarPlay.

CarPlaySceneDelegate (new) — manages the CPTemplateApplicationSceneDelegate lifecycle. On connect, loads the configured sitemap and renders it as a CPGridTemplate with openHAB SF symbol icons (mapped from openHAB icon names). Subscribes to SitemapEventStream SSE events and applies widget updates in real time; reconnects on network change or watchdog timeout.

  • Sitemap selection is stored in Preferences as carPlaySitemapName
  • SF symbol mapping replaces remote icon fetches to keep CarPlay rendering fast and offline-capable
  • Shows a placeholder CPListTemplate when no compatible sitemap widgets are found or CarPlay is not yet configured
  • Entitlements updated to include com.apple.developer.carplay-maps (via openHAB-CarPlay.entitlements)

ItemEventStream improvements

  • SSE watchdog — background task checks every 10 s whether any event has been received in the last 30 s; reconnects automatically on silence
  • Self-contained network monitoringstartMonitoringNetworkIfNeeded keeps the network-change loop alive for the actor's lifetime; startMonitoringNetwork now returns immediately instead of blocking

iOS 17 clean-up

Removes the #available(iOS 17) guard and the hand-rolled NWPathMonitor.paths() compatibility shim from NWPathMonitoring.swift now that iOS 16 is no longer supported.


Test plan

Widgets

  • Add a small/medium/large Switch widget to the Home Screen; configure a Home and items
  • Tap a switch in the widget → toggle command sent, widget state updates
  • Add a Sensor widget; verify current item state is displayed
  • Change item state via openHAB UI → widget refreshes within seconds (SSE) while app is active
  • Background / lock screen: widget refreshes on next 5-minute timeline reload

Sitemap SSE

  • SSE-capable server (openHAB ≥ 3.3): sitemap updates arrive as widget events without full page reloads
  • Server without SSE (openHAB < 3.3): falls back to long-polling via the existing JSON REST API
  • Network drop mid-SSE: reconnects and triggers a page refresh to recover missed events
  • Server restart: watchdog detects stale connection within 30 s and reconnects
  • Force-airplane-mode then restore: polling recovers without hang

Wide-display grid

  • Open a sitemap on iPad (or wide iPhone landscape) → rows appear in a two-column grid
  • Image, video, chart, webview, and map widgets span full width; other widgets share a half-width cell
  • Landscape images fill the available width without cropping
  • GIF images preserve their aspect ratio at full width
  • Video rows resize proportionally with the available width (no fixed 200 pt height)
  • On compact width (iPhone portrait) → standard list layout unchanged

CarPlay

  • Connect a CarPlay-capable device or use the CarPlay simulator in Xcode
  • Without a sitemap configured: placeholder screen is shown with setup instructions
  • Configure a sitemap in Settings → CarPlay; reconnect → grid of items appears with SF symbol icons
  • Change item state via openHAB UI → CarPlay grid updates in real time via SSE
  • Network drop mid-SSE: CarPlay reconnects and refreshes automatically
  • No compatible widgets in the sitemap: placeholder list is shown instead of an empty grid

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 65 out of 68 changed files in this pull request and generated no comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 66 out of 69 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@timbms
timbms force-pushed the feature/widgets-with-item-display branch from 4fd6b91 to b576981 Compare January 29, 2026 07:41
@timbms
timbms changed the base branch from develop to openapigen-swiftui February 23, 2026 08:53
@TAKeanice
TAKeanice force-pushed the openapigen-swiftui branch from 1f242b5 to 166a59c Compare March 26, 2026 19:49
Base automatically changed from openapigen-swiftui to develop March 30, 2026 19:59
@timbms timbms mentioned this pull request Apr 12, 2026
@timbms
timbms changed the base branch from develop to migrateToAppIntents2 May 16, 2026 17:50
Base automatically changed from migrateToAppIntents2 to develop May 27, 2026 09:09
@timbms
timbms force-pushed the feature/widgets-with-item-display branch 5 times, most recently from 35d5dea to 25cdb53 Compare May 28, 2026 09:24
Adds a widget extension with switch and sensor widgets.

Switch widget:
- Available in small (1 item), medium (2 items), and large (4 items)
  as separate widget registrations, so the configuration UI shows
  exactly the right number of item slots for the selected size
- Interactive toggle with optimistic visual update on tap (Toggle with
  custom PowerButtonToggleStyle, not Button)
- 600 ms pause in SetSwitchItemIntent.perform() prevents WidgetKit's
  immediate timeline refresh from racing openHAB's async command
  processing and overwriting the optimistic state flip
- State label ('ON'/'OFF') lives inside the ToggleStyle so it always
  agrees with the button colour, never the lagging timeline entry
- Medium/large use a row layout (bold label + compact circle toggle)
  with dividers; small shows a centred power button
- Previews and the widget gallery work without a configured home by
  rendering a static non-interactive circle button

Sensor widget:
- Displays current state of Number/String items

Common infrastructure:
- Delete OpenHABWidgetLiveActivity.swift: importing ActivityKit without
  Live Activity entitlements caused an EXC_GUARD/LIBXPC crash at
  extension startup before any Swift code ran
- SwitchSmall/Medium/LargeConfigurationAppIntent with matching
  SwitchSmall/Medium/LargeWidgetItemEntity types, each carrying an
  @IntentParameterDependency on its own configuration intent for
  home-filtered item selection in the picker
- openHAB icon overlay inset 14 pt from leading/top edge on all sizes

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
@timbms
timbms force-pushed the feature/widgets-with-item-display branch from 7cf0ab5 to a092c56 Compare May 28, 2026 11:38
timbms and others added 8 commits May 28, 2026 13:38
…itor

Port the two ItemEventStream improvements from feature/migration2SSE-minimal
without pulling in the unrelated sitemap-polling changes:

- Add networkMonitoringTask field: the network monitor now lives entirely
  inside the actor (started once via startMonitoringNetworkIfNeeded) instead
  of running an infinite for-await loop in the caller's task.
  startMonitoringNetwork() returns immediately after the first call.

- Add 30-second alive watchdog: a background Task inside listen(using:)
  checks lastEventTime every 10 s and reconnects if no event (including
  ALIVE heartbeats) has been received for 30 s. This recovers silently
  dead connections that occur after a server restart without a TCP RST.

Also fix WidgetItemMonitor.reloadWidgets(for:) which was still reloading
the deleted 'OpenHABSwitchWidget' kind; update to the three separate kinds
introduced when small/medium/large were split into separate widget structs.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
#1225)

- Bump exact versions in BuildTools/Package.swift and update Package.resolved
- Fix line_length violations newly flagged by SwiftLint 0.63.3: add disable
  comments in OpenHABSitemapWidgetEvent, OpenHABWidget, OpenHABImageProcessorTests,
  SitemapDiagnostics, and JSONParserTests
- Remove now-superfluous closure_end_indentation disables in TextInputRowView
  and DatePickerInputRowView
- SnapshotRowInputBuildResult: add explicit Sendable conformance (required by
  Task<Success: Sendable, …> in Swift 6) and guard it with
  // swiftformat:disable:next redundantSendable so the formatter leaves it intact

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
- Add OpenHABShortcuts: AppShortcutsProvider registering all 10 intents
  with Siri/Shortcuts phrases for iOS 17+
- Add AppShortcuts.strings for all 9 supported locales (en/de/es/fi/fr/it/nb/nl/ru)
  using the .strings format required by the iOS 16 deployment target
- Add resultValueName to GetItemStateIntent so its output is labeled in Shortcuts
- Add ProvidesDialog to SetSwitchItemIntent and SetPlayerValueIntent
- Simplify ItemEntity.displayRepresentation by removing the nonLocalizedDisplayString
  helper in favour of direct LocalizedStringResource string interpolation

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
AppIntentsMetadataProcessor builds an NLP vocabulary model for Siri using
each AppEnum case's DisplayRepresentation(title:synonyms:). Without synonyms
the processor allocates synonym slots in its model but finds them empty,
emitting 'unresolvable: empty template: Failed # variables.N.definitions.M.synomyms.K'
warnings for every empty slot across all locales.

Add 3 synonyms per case to SwitchAction, ContactState, and PlayerAction.
This both fills the NLP model slots and improves Siri's recognition of
different ways a user might name a switch action, contact state, or
player command.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
The AppIntentsMetadataProcessor in Xcode 15+ expects App Shortcuts phrase
localizations in String Catalog format (AppShortcuts.xcstrings). Using
individual lproj/AppShortcuts.strings files causes the processor to fail
to generate NLP synonym data for most locales, producing repeated
'unresolvable: empty template: Failed # variables.N.definitions.M.synomyms.K'
build warnings.

Consolidate all 9 locale translations (de, en, es, fi, fr, it, nb, nl, ru)
into a single AppShortcuts.xcstrings String Catalog and remove the
individual .strings files.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
AppShortcuts.xcstrings requires iOS 17.0+. Xcode set the minimum to
17.6 (latest iOS 17.x in the SDK) and watchOS to 10.6 accordingly.

Also includes Xcode-generated Localizable.xcstrings additions from
AppEnum caseDisplayRepresentations synonyms (extracted automatically
from d917708), and extractionState markers on secondary phrases in
multi-phrase App Shortcuts (Xcode extraction limitation, runtime
behaviour unchanged).

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
- OpenHABItemCache: add blank line after #endif (SwiftLint)
- Localizable.xcstrings: fill in missing translations for AppIntents
  string keys that were in 'new' state (SSE/AppIntents warnings)

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
timbms and others added 6 commits July 8, 2026 21:13
…ete translations

- Rename OpenHABIconOverlay.swift → OpenHABIconWatermark.swift to better
  reflect its purpose as a watermark overlay on widget preview images
- Complete missing translations in Localizable.xcstrings for all supported
  languages, resolving all NEW-state entries

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
…play

Merges CarPlay SSE streaming support and sitemap-based favorites scene.
Localizable.xcstrings resolved by preserving HEAD's multi-language
translations while adding carplay-sse's auto-generated comments and
new CarPlay-specific keys.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
- '%@ • %@': add de/es/fi/fr/it/nb/nl/ru (format string, identical to en)
- 'Number Item': add es/fi/nb/ru translations
- Local Network alert message: add es/fi/nb/ru translations

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
Three bugs fixed:

1. sitemapChanged and widget-notFound called startStreaming(), which
   cancelled and rebuilt the entire SSE stack (NetworkTracker reset +
   new subscription). The onTermination handler on the old continuation
   could fire while the new task was still doing network round-trips,
   causing SitemapEventStream.cleanupContinuation to cancel listenTask
   and create a gap in event coverage. Replace with schedulePageRefresh(),
   which re-fetches only the page without touching the SSE stream.

2. No page refresh on SSE reconnect: if SITEMAP_CHANGED was missed
   during a disconnect the page stayed permanently stale. Add a
   needsRefreshOnReconnect flag (matching SitemapPageViewModel) that
   triggers schedulePageRefresh() on the next .connected event.

3. Calling NetworkTracker.shared.startTracking() on every restart
   (sitemapChanged, notFound) could reset the shared tracker's active
   connection and briefly break the main app's SSE. Now startTracking
   is only called on initial connect or sitemap-selection change.

currentPage and currentService are now stored properties so
handleSseMessage and refreshPage can read them without being
threaded through every function parameter.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
@timbms
timbms force-pushed the feature/widgets-with-item-display branch from 6983abb to 7d57f63 Compare July 12, 2026 15:13
DigiH and others added 23 commits July 12, 2026 18:42
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
…ity alerts

ImageRowView: show an inline error card (icon + HTTP status message) when
a linked image fails to load instead of leaving a blank area. Clears on
URL change or successful reload.

SitemapPageView: exclude SitemapPageError (noActiveConnection,
serviceUnavailable, noData) from the modal alert — these are transient
connectivity states already reflected by the offline/loading indicators
and should not block the user.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
…th-item-display

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
…ts (#1298)

Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
…th-item-display

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
CPGridButton only supports a single text line, causing the setup
instruction to be clipped on all display sizes and in all languages.
Switch to CPInformationTemplate which renders the full text without
truncation.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
- Add parentWidgetId to OpenHABWidget via ObjC associated object (no
  stored-property layout change) and propagate it in flatten(_:); this
  was required for frame-child visibility tracking without corrupting
  Combine's @published field scan under concurrent test execution.

- Extract visibility logic into sitemapVisibleWidgets(_:) so both
  SitemapPageViewModel.relevantWidgets and the tests call the same code
  path instead of a duplicated helper.

- Fix frame visibility: empty frames (no children) are now hidden to
  match Android parity; previously only frames with children where all
  were invisible were hidden.

- Fix #expect crash in PreviewWidgetFactoryTests: extract non-Sendable
  OpenHABWidget properties to local Sendable variables before #expect
  to avoid null-function-pointer crash from -enable-actor-data-race-checks.

- Fix redundant try #require on already-unwrapped UIColor in NumberStateTests.

- Add emptyFrameIsHidden test.

- Remove stale FlexColorPicker pin from Package.resolved.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
…uttons

- Drop .button and .selection from CarPlay compatibility; neither type
  appears at page level in practice.
- Remove pushMappingList and segmented-switch handling; mapping lists
  are not a good fit for the CarPlay grid interaction model.
- Add press-release support: plain switch toggles ON/OFF; a switch with
  a single mapping carrying hasPressReleaseBehavior sends the press
  command, sleeps 500 ms, then sends the release command; a switch with
  widget-level releaseCommand is handled the same way.
- Exclude itemless Switch widgets (used as sitemap frames/containers)
  from the CarPlay grid via a widget.item != nil guard.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
Signed-off-by: DigiH <17110652+DigiH@users.noreply.github.com>
Adds translations for "None" and "Sitemap for CarPlay" picker labels,
and updates carplay_not_configured / carplay_not_configured_detail to
match the current English text (Switch items, iOS app settings) across
de, es, fi, fr, it, nb, nl, and ru.

Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
Signed-off-by: openhab-bot <info@openhabfoundation.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants