From 60deb43d2e4f435e35b7265ef9d3b9013cfa4721 Mon Sep 17 00:00:00 2001 From: Matzman Date: Mon, 5 Oct 2026 22:12:15 +0200 Subject: [PATCH] Add the "HA Dashboard" tab with HA's web UI (DSH-1) A second dashboard surface shows a WebEngineView at homeassistant.url, or a hint when none is set. HaWebUi (QML singleton owned by Dashboard) holds the URL and a persistent WebEngine profile under ~/.local/share/frameassistant/webengine so the HA login survives restarts. QtWebEngineQuick::initialize() runs in main() before the app object (D39). Co-Authored-By: Claude Opus 5.5 --- src/CMakeLists.txt | 10 +++++++-- src/Dashboard.cpp | 5 +++++ src/Dashboard.h | 4 ++++ src/ha/HaWebUi.cpp | 49 +++++++++++++++++++++++++++++++++++++++++ src/ha/HaWebUi.h | 46 ++++++++++++++++++++++++++++++++++++++ src/main.cpp | 5 +++++ src/qml/HaDashboard.qml | 40 +++++++++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/ha/HaWebUi.cpp create mode 100644 src/ha/HaWebUi.h create mode 100644 src/qml/HaDashboard.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5892416..1f48f40 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,14 +58,20 @@ target_link_libraries(frameassistant_lib PUBLIC Qt6::Core) target_compile_options(frameassistant_lib PRIVATE -Wall -Wextra) # The app: its UI is the QML module "FrameAssistant" (src/qml/). +# Qt WebEngine shows HA's web UI (DSH-1); on the Frame it comes with the system Qt. +find_package(Qt6 REQUIRED COMPONENTS WebEngineQuick) vrdash_add_dashboard(frameassistant OUTPUT_NAME ${VRDASH_EXECUTABLE_NAME} QML_URI FrameAssistant SOURCES main.cpp Dashboard.h Dashboard.cpp + QML_SOURCES ha/HaWebUi.h ha/HaWebUi.cpp QML_FILES qml/Main.qml - qml/Card.qml) -target_link_libraries(frameassistant PRIVATE frameassistant_lib) + qml/Card.qml + qml/HaDashboard.qml) +target_link_libraries(frameassistant PRIVATE frameassistant_lib Qt6::WebEngineQuick) +# The generated QML type registration includes QML_SOURCES headers by file name. +target_include_directories(frameassistant PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ha) install(FILES ${OPENVR_LIBRARY} DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/Dashboard.cpp b/src/Dashboard.cpp index 81ade1c..7279887 100644 --- a/src/Dashboard.cpp +++ b/src/Dashboard.cpp @@ -17,6 +17,9 @@ Dashboard::Dashboard(int& argc, char** argv) .arg(Config::defaultPath()), QStringLiteral("path"))}) { addSurface({.qmlRootType = QStringLiteral("Main")}); + addSurface({.id = QStringLiteral("dashboard"), + .qmlRootType = QStringLiteral("HaDashboard"), + .displayName = QStringLiteral("HA Dashboard")}); } bool Dashboard::prepare(QString* error) { @@ -34,3 +37,5 @@ bool Dashboard::prepare(QString* error) { << m_config.deviceName << "(" + m_config.deviceId + ")"; return true; } + +void Dashboard::loading() { m_haWebUi.setUrl(m_config.haUrl); } diff --git a/src/Dashboard.h b/src/Dashboard.h index 10d6eed..16fede5 100644 --- a/src/Dashboard.h +++ b/src/Dashboard.h @@ -2,6 +2,7 @@ #include "app/DashboardApplication.h" #include "config/Config.h" +#include "ha/HaWebUi.h" // The dashboard app. Its UI is the QML module set up in src/CMakeLists.txt // (QML_URI FrameAssistant, files in src/qml/). @@ -16,7 +17,10 @@ class Dashboard : public DashboardApplication { protected: // Loads the settings (overlay and desktop mode; docs/DESIGN.md, D22). bool prepare(QString* error) override; + void loading() override; private: Config m_config; + // QML singleton (HaWebUi); a member, so it outlives all QML. + HaWebUi m_haWebUi{nullptr}; }; diff --git a/src/ha/HaWebUi.cpp b/src/ha/HaWebUi.cpp new file mode 100644 index 0000000..f765ccb --- /dev/null +++ b/src/ha/HaWebUi.cpp @@ -0,0 +1,49 @@ +#include "ha/HaWebUi.h" + +#include +#include +#include + +namespace { + +HaWebUi* g_instance = nullptr; + +} // namespace + +HaWebUi::HaWebUi(QObject* parent) : QObject(parent) { + Q_ASSERT(!g_instance); + g_instance = this; +} + +HaWebUi::~HaWebUi() { g_instance = nullptr; } + +QQuickWebEngineProfile* HaWebUi::profile() { + if (!m_profile) { + m_profile = new QQuickWebEngineProfile(this); + m_profile->setStorageName(QStringLiteral("frameassistant")); + m_profile->setOffTheRecord(false); + m_profile->setPersistentStoragePath(storagePath()); + m_profile->setCachePath(cachePath()); + m_profile->setPersistentCookiesPolicy( + QQuickWebEngineProfile::AllowPersistentCookies); + } + return m_profile; +} + +QString HaWebUi::storagePath() { + return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + + QStringLiteral("/frameassistant/webengine"); +} + +QString HaWebUi::cachePath() { + return QStandardPaths::writableLocation( + QStandardPaths::GenericCacheLocation) + + QStringLiteral("/frameassistant/webengine"); +} + +HaWebUi* HaWebUi::create(QQmlEngine*, QJSEngine*) { + Q_ASSERT(g_instance); + // Owned by the app, not by the QML engine. + QJSEngine::setObjectOwnership(g_instance, QJSEngine::CppOwnership); + return g_instance; +} diff --git a/src/ha/HaWebUi.h b/src/ha/HaWebUi.h new file mode 100644 index 0000000..4cd02eb --- /dev/null +++ b/src/ha/HaWebUi.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include + +class QJSEngine; +class QQmlEngine; + +// HA's web UI in the "HA Dashboard" tab (DSH-1): the URL from the settings +// and a persistent WebEngine profile, so the HA login survives restarts +// (docs/DESIGN.md, D39). A QML singleton owned by the app (Dashboard). +class HaWebUi : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + // Empty: no URL configured (the tab shows a hint). + Q_PROPERTY(QUrl url READ url CONSTANT) + Q_PROPERTY(QQuickWebEngineProfile* profile READ profile CONSTANT) + + public: + // Not default-constructible, so QML uses create() instead of making its + // own instance. + explicit HaWebUi(QObject* parent); + ~HaWebUi() override; + + // Call before the QML is loaded. + void setUrl(const QUrl& url) { m_url = url; } + + QUrl url() const { return m_url; } + // Created on first use: only a tab that shows the web UI needs it. + QQuickWebEngineProfile* profile(); + + // Where the profile keeps its data (cookies, local storage) and cache. + static QString storagePath(); + static QString cachePath(); + + // QML singleton factory: the instance the app created. + static HaWebUi* create(QQmlEngine*, QJSEngine*); + + private: + QUrl m_url; + QQuickWebEngineProfile* m_profile = nullptr; +}; diff --git a/src/main.cpp b/src/main.cpp index b2aa26b..49fe89d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,11 @@ +#include + #include "Dashboard.h" int main(int argc, char* argv[]) { + // Must run before the Qt application object exists (the Dashboard + // constructor creates it). + QtWebEngineQuick::initialize(); Dashboard app(argc, argv); return app.exec(); } diff --git a/src/qml/HaDashboard.qml b/src/qml/HaDashboard.qml new file mode 100644 index 0000000..7657ca4 --- /dev/null +++ b/src/qml/HaDashboard.qml @@ -0,0 +1,40 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtWebEngine + +// "HA Dashboard" tab (DSH-1): HA's own web UI at the URL from the settings. +// Without a URL it shows a hint instead. +Pane { + id: root + + Material.theme: Material.Dark + padding: 0 + font.pixelSize: 24 + + Loader { + anchors.fill: parent + sourceComponent: HaWebUi.url.toString() !== "" ? webUi : hint + } + + Component { + id: webUi + + WebEngineView { + profile: HaWebUi.profile + url: HaWebUi.url + } + } + + Component { + id: hint + + Label { + padding: 48 + wrapMode: Text.WordWrap + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: qsTr("No Home Assistant URL configured.\n\nSet \"homeassistant.url\" in the settings file and restart.") + } + } +}