From 1bca71ff7ed604517e972aff88f28c7398650b49 Mon Sep 17 00:00:00 2001 From: veenm Date: Sat, 31 Oct 2026 18:48:32 +0100 Subject: [PATCH] Dashboard Studio 2.6.0 and Haven 2.3.0: easier to use on a phone Co-Authored-By: Claude Opus 5.5 --- dashboard_studio/CHANGELOG.md | 15 ++ dashboard_studio/DOCS.md | 7 +- dashboard_studio/config.yaml | 2 +- dashboard_studio/web/app.js | 105 +++++++++-- dashboard_studio/web/haven.apk | Bin 91778 -> 99970 bytes dashboard_studio/web/haven.json | 2 +- dashboard_studio/web/i18n.js | 12 +- dashboard_studio/web/index.html | 2 +- dashboard_studio/web/style.css | 22 +++ haven/AndroidManifest.xml | 7 +- haven/assets/setup.html | 56 +++++- .../src/nl/melvanveen/haven/MainActivity.java | 172 +++++++++++++++++- 12 files changed, 365 insertions(+), 37 deletions(-) diff --git a/dashboard_studio/CHANGELOG.md b/dashboard_studio/CHANGELOG.md index def8cce..5a5722f 100644 --- a/dashboard_studio/CHANGELOG.md +++ b/dashboard_studio/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 2.6.0 + +Easier to use on a phone, in the browser and in Haven (now version 2.3.0, offered as an update in the app). + +- **Back closes what's open:** Android's back gesture in Haven, and the browser's back button, now close an open sheet or the top bar instead of leaving the app. +- **Haven finds your Home Assistant:** when you set it up or add a home, Haven lists the Home Assistant it finds on your home network. Tap it instead of typing the address. +- **Haven keeps only homes that work:** an address that doesn't connect is no longer saved as a home; it is filled in again so you can correct it. Removing a home asks first, in the app itself. +- **Haven says what it's doing:** the loading screen says which home it is connecting to, and after a while offers to choose another home or address. +- **A phone opens a phone dashboard:** on a phone, Dashboard Studio opens the dashboard you last used on that phone, or else the first one designed for phones; and always on the dashboard, not the builder. +- **Connecting, not offline:** while it first connects, the top bar says *Connecting…* instead of a red *Offline*. +- **The ⋯ button steps aside:** on a phone dashboard, the round button in the corner moves out of the way while you scroll down, so it doesn't cover a card, and comes back when you scroll up. +- **Friendlier details:** tapping a sensor or device shows what it is now in words, a line of the last 24 hours for sensors, and buttons for blinds, media players and vacuums. The entity ID and attributes are folded away under *Technical details*. +- Weather reads *Partly cloudy* instead of *Partlycloudy* (and the same for a few other states). +- Fixed: on a lit lamp tile in the phone sizes, the text was white on a pale lamp colour. + ## 2.5.0 - **Phone sizes:** on a phone dashboard every card now comes in four sizes, like widgets on a phone: **Small** (half width, square), **Medium** (full width, two rows), **Large** (full width, four rows) and **Row** (full width, one line). Pick one under **Size on the phone** in the inspector, or drag the corner: it snaps to the nearest size. diff --git a/dashboard_studio/DOCS.md b/dashboard_studio/DOCS.md index 1173466..1546bcf 100644 --- a/dashboard_studio/DOCS.md +++ b/dashboard_studio/DOCS.md @@ -43,10 +43,10 @@ Under **Address for the link**, use the address of Home Assistant on your home n Haven shows the dashboards and nothing else: no builder and no Admin screen, for admins too. 1. On the phone, scan the QR code under **Admin → Haven app** (or tap **Download Haven** on the phone itself), open the file and allow installing apps from that source when Android asks (once). -2. Open Haven and enter the address you use for Home Assistant. On the phone itself, **Open in Haven** fills it in. +2. Open Haven and tap your Home Assistant under **On this network** (Haven finds it on your home Wi-Fi), or enter the address you use for it. On the phone itself, **Open in Haven** fills it in. 3. Sign in with your own Home Assistant login, once. -From then on Haven opens straight on your dashboard, full screen with its own icon, and keeps you signed in. It works with plain http addresses too. Kiosk mode keeps the screen on and hides Android's bars. To connect to another Home Assistant: tap the phone button in the top bar (admins) or the kiosk corner five times (everyone else), then **Change address**. Haven needs Android 7 or newer. The earlier *Dashboard Studio* Android app is replaced by Haven; you can uninstall it. +From then on Haven opens straight on your dashboard, full screen with its own icon, and keeps you signed in. It works with plain http addresses too. Kiosk mode keeps the screen on and hides Android's bars. To add another home, such as a holiday home, or switch between them: tap the phone button in the top bar (admins) or the kiosk corner five times (everyone else), then **Homes and addresses**. An address that doesn't connect isn't kept. If connecting takes long, the loading screen offers to choose another home or address. Android's back gesture closes an open sheet first; with nothing open, it puts Haven in the background. Haven needs Android 7 or newer. The earlier *Dashboard Studio* Android app is replaced by Haven; you can uninstall it. - **Several homes:** under **Change address**, **Add another home** connects Haven to another Home Assistant, such as a holiday home or your parents'. Each home keeps its own sign-in; tap a home to open it, × removes it. - **Home screen widgets:** long-press your home screen, choose **Widgets → Haven**, and pick a light, switch, scene or thermostat. A tap does what it does on the dashboard; a thermostat gets − and + for its temperature. @@ -297,6 +297,9 @@ On a phone, such a dashboard fills the whole screen, without the top bar with th ### On phones +- **Which dashboard opens:** on a phone, Dashboard Studio opens the dashboard last used on that phone, or else the first one designed for phones (a link, the device's own dashboard or the person's dashboard still come first). It always opens on the dashboard, not the builder. +- **Details:** tap a sensor or device for its details: what it is now, a line of the last 24 hours for sensors, and its buttons. The entity ID and attributes are under *Technical details*. Back (the gesture, or the browser's button) closes the sheet. + - **Install as an app:** on Android, use **Haven** (see above). The phone button in the top bar explains the other options: browsers can install Dashboard Studio as a web app when Home Assistant is opened over **https** (open Dashboard Studio in its own tab first); on iPhone and iPad, apps on the home screen can't use Home Assistant's sign-in, so use the sidebar in the Home Assistant app. - **Shortcuts:** tick **Shortcut on the app icon** in the settings of a button, tile, light, thermostat or garage card. When Dashboard Studio is installed from the browser on Android, long-press its icon to run one straight away (up to four shortcuts; Haven and iPhones don't support this). - **Offline:** each device keeps the last known states of its dashboard. When the connection drops, the dashboard keeps showing them with a small note saying from when, and catches up when it reconnects. diff --git a/dashboard_studio/config.yaml b/dashboard_studio/config.yaml index b0cdac6..1537529 100644 --- a/dashboard_studio/config.yaml +++ b/dashboard_studio/config.yaml @@ -1,5 +1,5 @@ name: Dashboard Studio -version: "2.5.0" +version: "2.6.0" slug: dashboard_studio description: Drag-and-drop dashboard builder with a free-form grid, live entity widgets and themes. url: https://git.melvanveen.nl/veenm/ha-dashboard-studio diff --git a/dashboard_studio/web/app.js b/dashboard_studio/web/app.js index 08814a6..ea06347 100644 --- a/dashboard_studio/web/app.js +++ b/dashboard_studio/web/app.js @@ -301,8 +301,8 @@ const p = (t, cls = "") => `${esc(t)}

`; let body, actions = ``; if (IN_APP) { - body = p(tr("This is Haven, connected to {0}.", location.host)); - actions += ``; + body = p(tr("This is Haven, connected to {0}. You can add another home, such as a holiday home, or switch between them.", location.host)); + actions += ``; } else if (android) { // A home screen app from the browser needs https, and Home Assistant's own app can't make one at all: Haven always works. body = p(tr("Get Haven, the Android app for your dashboards. It opens full screen with its own icon and keeps you signed in.")) @@ -325,7 +325,7 @@ if (browserInstall) actions += ``; } if (!IN_APP && !android && isAdmin()) body += p(tr("For Android phones there is Haven: see Admin, Haven app."), "muted small"); - const choice = await openModal(`

${tr("Install as an app")}

${body} + const choice = await openModal(`

${IN_APP ? "Haven" : tr("Install as an app")}

${body} ${IN_APP || ios ? "" : p(tr("It opens on the dashboard chosen for that device. Mark buttons as a shortcut to use them from the app icon."), "muted small")} `, (e, close) => { const x = e.target.closest("[data-x], a.btn"); if (x) close(x.dataset.x || "link"); }); @@ -2238,15 +2238,34 @@ // ------------------------------------------------------------ dialogs let modalClose = null; + // An open sheet is a step in the history, so going back (Android's back gesture in Haven, the browser's + // back button) closes it instead of leaving the app. + let modalInHistory = false, closingByBack = false, ownBacks = 0; + window.addEventListener("popstate", () => { + if (ownBacks) { ownBacks--; return; } // the step back a closing sheet took itself (another may have opened since) + if (!modalInHistory) return; + modalInHistory = false; + closingByBack = true; + modalClose?.(null); + closingByBack = false; + }); + /** Haven's back gesture asks first: close what is open here (a sheet, the top bar) and say so. */ + window.havenBack = () => { + if (modalClose) { modalClose(null); return true; } + if (document.body.classList.contains("peek")) { document.body.classList.remove("peek"); return true; } + return false; + }; function openModal(html, onClick) { const card = $("#modal .modal-card"); card.innerHTML = html; $("#modal").hidden = false; + if (!modalInHistory) { history.pushState({ dsSheet: true }, "", location.href); modalInHistory = true; } const prevFocus = document.activeElement; return new Promise((resolve) => { modalClose = (result) => { $("#modal").hidden = true; modalClose = null; + if (modalInHistory && !closingByBack) { modalInHistory = false; ownBacks++; history.back(); } prevFocus?.focus?.(); resolve(result); }; @@ -2467,23 +2486,56 @@ }); } + /** The details of one entity: what it is now in words, a day's history for a sensor, the controls, + * and the technical details (its ID and attributes) folded away for whoever needs them. */ function moreInfo(eid) { const s = ha.states[eid]; if (!s) return; - if (domainOf(eid) === "light") { lightPopup(eid); return; } - if (domainOf(eid) === "climate") { climatePopup(eid); return; } - const toggle = window.HAFmt.TOGGLE_DOMAINS.has(domainOf(eid)); - const actionLabel = domainOf(eid) === "lock" ? (s.state === "locked" ? tr("Unlock") : tr("Lock")) : ["scene", "script"].includes(domainOf(eid)) ? tr("Run") : isOn(s) ? tr("Turn off") : tr("Turn on"); - openModal(`

${esc(friendly(s))}

${esc(eid)}
-
${esc(fmtState(s))}
+ const d = domainOf(eid); + if (d === "light") { lightPopup(eid); return; } + if (d === "climate") { climatePopup(eid); return; } + const toggle = window.HAFmt.TOGGLE_DOMAINS.has(d) && !["climate", "vacuum", "media_player", "cover"].includes(d); + const actionLabel = d === "lock" ? (s.state === "locked" ? tr("Unlock") : tr("Lock")) : ["scene", "script"].includes(d) ? tr("Run") : ["button", "input_button"].includes(d) ? tr("Press") : isOn(s) ? tr("Turn off") : tr("Turn on"); + const numeric = ["sensor", "input_number", "number", "counter"].includes(d) && s.state !== "" && isFinite(Number(s.state)); + // a few domains get their own buttons + const svc = (service, ico, label) => ``; + const extra = d === "cover" ? svc("cover.open_cover", "arrow-up", tr("Open")) + svc("cover.stop_cover", "stop", tr("Stop")) + svc("cover.close_cover", "arrow-down", tr("Close")) + : d === "media_player" ? svc("media_player.media_previous_track", "prev", tr("Previous track")) + svc("media_player.media_play_pause", s.state === "playing" ? "pause" : "play", s.state === "playing" ? tr("Pause") : tr("Play")) + svc("media_player.media_next_track", "next", tr("Next track")) + : d === "vacuum" ? svc(s.state === "cleaning" ? "vacuum.pause" : "vacuum.start", s.state === "cleaning" ? "pause" : "play", s.state === "cleaning" ? tr("Pause") : tr("Start")) + svc("vacuum.return_to_base", "home", tr("Dock")) : ""; + const attrs = Object.entries(s.attributes).filter(([k]) => !["friendly_name", "icon", "entity_picture", "supported_features"].includes(k)); + const done = openModal(`
${window.HAFmt.icon(s)}

${esc(friendly(s))}

+
${esc(fmtState(s))}
${esc(tr("Changed {0}", relTime(s.last_changed)))}
- ${Object.entries(s.attributes).map(([k, v]) => ``).join("")}
${esc(cap(k))}${esc(typeof v === "object" ? JSON.stringify(v) : v)}
+ ${numeric ? `
${tr("Loading history…")}
` : ""} + ${extra ? `
${extra}
` : ""} +
${tr("Technical details")} + ${attrs.map(([k, v]) => ``).join("")}
${tr("Entity ID")}${esc(eid)}
${esc(cap(k))}${esc(typeof v === "object" ? JSON.stringify(v) : v)}
`, (e, close) => { - const x = e.target.closest("[data-x]")?.dataset.x; - if (x === "toggle") defaultAction(eid); - if (x) close(); + const b = e.target.closest("[data-x], [data-svc]"); + if (!b) return; + if (b.dataset.svc) { const [dom, service] = b.dataset.svc.split("."); ha.callService(dom, service, {}, eid); haptic(); return; } + if (b.dataset.x === "toggle") defaultAction(eid); + close(); }); + if (numeric) drawDetailGraph(eid, done); + } + + /** A day of a sensor's history as a line, with its lowest and highest value. */ + async function drawDetailGraph(eid, done) { + let pts = []; + try { pts = ((await ha.history([eid], 24))[eid] || []).filter(([, v]) => isFinite(v)); } catch { /* shown as unavailable */ } + const box = $("#modal:not([hidden]) .mi-graph"); + if (!box) return; + if (pts.length < 2) { box.innerHTML = `
${tr("No history for the last 24 hours.")}
`; return; } + const W_ = 320, H = 90, t0 = Date.now() - 24 * 3600e3, t1 = Date.now(); + const vs = pts.map(([, v]) => v), lo = Math.min(...vs), hi = Math.max(...vs), span = hi - lo || 1; + const x = (t) => ((Math.max(t, t0) - t0) / (t1 - t0)) * W_, y = (v) => H - 6 - ((v - lo) / span) * (H - 12); + const line = pts.map(([t, v], i) => `${i ? "L" : "M"}${x(t).toFixed(1)} ${y(v).toFixed(1)}`).join("") + `L${W_} ${y(vs[vs.length - 1]).toFixed(1)}`; + const unit = ha.states[eid]?.attributes.unit_of_measurement || ""; + const f = (v) => `${Number(v).toLocaleString(LOCALE, { maximumFractionDigits: 1 })}${unit ? ` ${unit}` : ""}`; + box.innerHTML = ` +
${tr("24 hours ago")}${esc(tr("Low {0} · high {1}", f(lo), f(hi)))}${tr("Now")}
`; } /** The changelog in a dialog. A small Markdown subset: ## headings, - bullets, **bold** and *italic*. */ @@ -2599,7 +2651,7 @@ const others = lockedKiosk ? store.dashboards.filter((d) => d.id !== current.id) : []; const choice = await openModal(lockedKiosk ? `

${tr("Dashboards")}

${me?.guest && me.until ? `

${esc(tr("This invite ends {0}.", new Date(me.until * 1000).toLocaleString(LOCALE, { weekday: "long", hour: "2-digit", minute: "2-digit" })))}

` : ""}${others.length ? `
${others.map((d) => ``).join("")}
` : `

${tr("This is the only dashboard you can see. An admin can add more for you.")}

`} - ` + ` : `

${tr("Leave kiosk mode?")}

${tr("This closes by itself in a few seconds.")}

`, (e, close) => { const x = e.target.closest("[data-x]")?.dataset.x; if (x) close(x); }); @@ -3003,11 +3055,23 @@ if (on) peekTimer = setTimeout(() => document.body.classList.remove("peek"), 15000); } $("#phone-menu").onclick = () => peek(!document.body.classList.contains("peek")); + // Scrolling down, the ⋯ button steps aside so it doesn't sit on a card; scrolling up brings it back. + let lastScrollY = 0; + document.addEventListener("scroll", (e) => { + if (!document.body.classList.contains("phone-ui")) return; + const el = e.target === document ? document.scrollingElement : e.target; + if (!el || el.closest?.(".w, #modal")) return; // a list inside a card, or a sheet: not the page + const y = el.scrollTop || 0; + if (Math.abs(y - lastScrollY) < 6) return; + document.body.classList.toggle("menu-away", y > lastScrollY && y > 40); + lastScrollY = y; + }, { capture: true, passive: true }); // ------------------------------------------------------------ chrome wiring function setCurrent(id) { current = store.dashboards.find((d) => d.id === id) || store.dashboards[0]; lsSet("ds.current", current.id); + if (phoneScreen.matches) lsSet("ds.currentPhone", current.id); activeTabId = lsGet(`ds.tab.${current.id}`); guestOn = false; guestCache = null; awayState = null; awayFrom = null; undoStack.length = redoStack.length = 0; @@ -3721,10 +3785,14 @@ document.querySelectorAll("#menu [data-icon], #mode-seg [data-icon]").forEach((b) => b.insertAdjacentHTML("afterbegin", Icons.svg(b.dataset.icon))); } + let everConnected = false; ha.onStatus = (ok) => { const c = $("#conn"); + if (ok) everConnected = true; c.classList.toggle("ok", ok); - c.textContent = ok ? tr("Connected") : offlineText(); + // the first moments after opening are connecting, not offline + c.classList.toggle("connecting", !ok && !everConnected && !lastLive); + c.textContent = ok ? tr("Connected") : !everConnected && !lastLive ? tr("Connecting…") : offlineText(); showOffline(!ok); // An update restarts the app, which drops the connection: check right after reconnecting. if (ok) checkVersion(); @@ -3824,9 +3892,12 @@ const exists = (id) => id && store.dashboards.some((d) => d.id === id); // The screen it was on last time; people who aren't admins, and Haven, only get the dashboard. const lastMode = lsGet("ds.mode") || (lsGet("ds.edit") === "1" ? "builder" : "dashboard"); - mode = isAdmin() && !IN_APP ? lastMode : "dashboard"; + // the builder is for bigger screens: a phone always opens on the dashboard + mode = isAdmin() && !IN_APP && !phoneScreen.matches ? lastMode : "dashboard"; editMode = mode === "builder"; - setCurrent([hashId, dev?.dashboard, home, lsGet("ds.current")].find(exists)); + // On a phone: the one last used on this phone, else the first dashboard designed for phones. + const phone = phoneScreen.matches ? [lsGet("ds.currentPhone"), store.dashboards.find((d) => d.mobile)?.id] : []; + setCurrent([hashId, dev?.dashboard, home, ...phone, lsGet("ds.current")].find(exists)); if (!isAdmin()) { lockedKiosk = true; enterKiosk({ fullscreen: false }); } else if (location.hash.includes("kiosk") || shared.get("ds_kiosk") === "1" || (dev?.kiosk && !hashId)) enterKiosk({ fullscreen: false }); else setMode(mode); diff --git a/dashboard_studio/web/haven.apk b/dashboard_studio/web/haven.apk index 260bad6d8466864d21ef7a46959af8de3f1007d4..53fb850029ddf3c8115ae698c115d68722b4408a 100644 GIT binary patch delta 40234 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�=TEewveCJyNE>Ng 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 diff --git a/dashboard_studio/web/haven.json b/dashboard_studio/web/haven.json index 7fdabfb..c711432 100644 --- a/dashboard_studio/web/haven.json +++ b/dashboard_studio/web/haven.json @@ -1 +1 @@ -{"version": "2.2.0", "code": 2} +{"version": "2.3.0", "code": 3} diff --git a/dashboard_studio/web/i18n.js b/dashboard_studio/web/i18n.js index a6d0043..05d63cc 100644 --- a/dashboard_studio/web/i18n.js +++ b/dashboard_studio/web/i18n.js @@ -303,6 +303,10 @@ "Emptying": "Aan het legen", "Levelling": "Aan het egaliseren", "Sanitizing": "Aan het ontsmetten", "Sleeping": "Slaapt", "Ready": "Klaar voor gebruik", "Bin full": "Afvalbak vol", "Check the litter box": "Kijk even bij de kattenbak", "Cat inside": "Kat in de bak", "Needs attention": "Heeft aandacht nodig", "visits today": "bezoekjes vandaag", "visits": "bezoekjes", "visit": "bezoekje", + "Connecting…": "Verbinden…", "Technical details": "Technische details", "Entity ID": "Entiteit-ID", "The last 24 hours": "De afgelopen 24 uur", + "No history for the last 24 hours.": "Geen geschiedenis van de afgelopen 24 uur.", "24 hours ago": "24 uur geleden", "Low {0} · high {1}": "Laagst {0} · hoogst {1}", + "Homes and addresses": "Huizen en adressen", + "This is Haven, connected to {0}. You can add another home, such as a holiday home, or switch between them.": "Dit is Haven, verbonden met {0}. Je kunt nog een huis toevoegen, zoals een vakantiehuis, of tussen huizen wisselen.", "Size on the phone": "Formaat op de telefoon", "Small": "Klein", "Medium": "Middel", "Now {0}°": "Nu {0}°", "Nobody home": "Niemand thuis", "Tap to watch": "Tik om te kijken", "Returning": "Terug naar het dock", "Sun {0}": "Zon {0}", "Grid {0}": "Net {0}", "Sending back {0}": "Teruglevering {0}", "{0} open": "{0} open", "Nothing on the way": "Niets onderweg", "Partly open": "Half open", "{0} of {1} open": "{0} van {1} open", "Nothing planned": "Niets gepland", "Only show when the bin is full or something is wrong": "Alleen tonen als de afvalbak vol is of er iets mis is", "Clean": "Schoonmaken", "Auto": "Auto", "in the box today": "in de bak vandaag", "cat’s weight": "gewicht kat", "Clean now": "Nu schoonmaken", "Auto clean": "Automatisch schoonmaken", @@ -811,6 +815,12 @@ armed_home: "Ingeschakeld (thuis)", armed_night: "Ingeschakeld (nacht)", disarmed: "Uitgeschakeld", triggered: "Geactiveerd", }; + // States whose names don't read well as they are ("partlycloudy", "not_home"). + const STATES_EN = { + not_home: "Away", partlycloudy: "Partly cloudy", "clear-night": "Clear night", "lightning-rainy": "Thunder and rain", "snowy-rainy": "Sleet", + "windy-variant": "Windy", heat_cool: "Heat and cool", fan_only: "Fan only", above_horizon: "Above the horizon", below_horizon: "Below the horizon", + armed_away: "Armed (away)", armed_home: "Armed (home)", armed_night: "Armed (night)", armed_vacation: "Armed (vacation)", disarmed: "Disarmed", + }; const read = (k) => { try { return localStorage.getItem(k); } catch { return null; } }; function detect() { const pref = read("ds.lang"); @@ -837,7 +847,7 @@ return args.length ? s.replace(/\{(\d)\}/g, (_, i) => args[i] ?? "") : s; }, /** A Home Assistant state as readable text. */ - state: (st) => (lang === "nl" && STATES_NL[st]) || cap(st), + state: (st) => (lang === "nl" ? STATES_NL[st] : STATES_EN[st]) || cap(st), }; document.documentElement.lang = lang; })(); diff --git a/dashboard_studio/web/index.html b/dashboard_studio/web/index.html index 8830f23..fb93a5f 100644 --- a/dashboard_studio/web/index.html +++ b/dashboard_studio/web/index.html @@ -30,7 +30,7 @@
- Offline + Connecting… diff --git a/dashboard_studio/web/style.css b/dashboard_studio/web/style.css index 4fba83f..eb02a77 100644 --- a/dashboard_studio/web/style.css +++ b/dashboard_studio/web/style.css @@ -125,6 +125,10 @@ h2, h3 { margin: 0; } .conn { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--crit); padding: 0 6px; } .conn::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; } .conn.ok { display: none; } +.conn.connecting { color: var(--text-2); } +.conn.connecting::before { animation: conn-pulse 1.2s ease-in-out infinite; } +@keyframes conn-pulse { 50% { opacity: .3; } } +@media (prefers-reduced-motion: reduce) { .conn.connecting::before { animation: none; } } .save-status { font-size: 13px; color: var(--text-2); padding: 0 6px; white-space: nowrap; } .menu { @@ -1215,6 +1219,8 @@ body.phone-ui:not(.peek) #topbar { display: none; } background: rgba(120, 120, 128, .28); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); color: var(--text); box-shadow: 0 2px 8px rgba(0, 0, 0, .15); } #phone-menu svg { width: 20px; height: 20px; stroke-width: 2.6; } body.phone-ui:not(.peek):not(.kiosk) #phone-menu { display: grid; } +#phone-menu { transition: opacity .2s, transform .2s; } +body.menu-away #phone-menu { opacity: 0; transform: translateY(-12px); pointer-events: none; } /* the install button in the top bar is for phones and tablets; computers have it in the menu */ @media (pointer: fine) { #btn-app { display: none; } } #btn-app[hidden] { display: none; } @@ -2142,3 +2148,19 @@ button.fader-input { cursor: pointer; } .phc-m:not(:has(.phc-bottom)) { justify-content: center; } .phc-m:not(:has(.phc-bottom)) .phc-name { font-size: 20px; } .phc-l:not(:has(.phc-val)) .phc-name { font-size: 24px; } + +/* the details sheet: the state in words, a day's history, the controls; the technical details folded away */ +.mi-head { display: flex; align-items: center; gap: 10px; } +.mi-head h3 { margin: 0; } +.mi-ico { display: flex; font-size: 22px; color: var(--text-2); } +.mi-state { font-size: 34px; font-weight: 400; letter-spacing: -0.02em; margin: 14px 0 2px; } +.mi-graph { margin-top: 14px; } +.mi-graph svg { display: block; width: 100%; height: 90px; } +.mi-graph-legend { display: flex; justify-content: space-between; gap: 8px; margin-top: 4px; } +.mi-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } +.mi-actions .btn { flex: 1; min-width: 0; justify-content: center; } +.mi-more { margin-top: 16px; } +.mi-more summary { cursor: pointer; font-size: 13px; color: var(--text-2); padding: 6px 0; } +.mi-more .attr-table { margin-top: 4px; } +/* lit tiles keep the lamp's dark text */ +.phc { color: inherit; } diff --git a/haven/AndroidManifest.xml b/haven/AndroidManifest.xml index 2fd8c03..bad0c48 100644 --- a/haven/AndroidManifest.xml +++ b/haven/AndroidManifest.xml @@ -1,13 +1,16 @@ + android:versionCode="3" + android:versionName="2.3.0"> + + + diff --git a/haven/assets/setup.html b/haven/assets/setup.html index 5b329f4..d61dcb5 100644 --- a/haven/assets/setup.html +++ b/haven/assets/setup.html @@ -28,6 +28,21 @@ .home .x { width: 44px; flex: none; background: transparent; color: #a9b8c2; font-size: 20px; padding: 12px 0; } .add { background: transparent; color: #f4a531; border: 1.5px solid rgba(244, 165, 49, .5); margin-bottom: 22px; } #f.hide { display: none; } + .found { margin: 0 0 22px; } + .found h2 { font-size: 14px; font-weight: 600; margin: 0 0 8px; } + .found .go { display: flex; flex-direction: column; text-align: left; background: rgba(255, 255, 255, .08); color: #f4eee4; padding: 12px 14px; margin-bottom: 8px; } + .found .go small { font-weight: 400; color: #a9b8c2; font-size: 13px; } + .found .look { display: flex; align-items: center; gap: 10px; font-size: 14px; color: #a9b8c2; margin: 0 0 6px; } + .found .look::before { content: ""; width: 14px; height: 14px; border-radius: 50%; border: 2px solid #f4a531; border-right-color: transparent; animation: spin .9s linear infinite; flex: none; } + .found .look.done::before { display: none; } + .found .look[hidden] { display: none; } + @keyframes spin { to { transform: rotate(360deg); } } + .or { font-size: 14px; color: #a9b8c2; margin: 0 0 12px; } + .home.ask { flex-wrap: wrap; background: rgba(255, 69, 58, .12); border-radius: 12px; padding: 12px 14px; } + .home.ask p { flex-basis: 100%; margin: 0 0 10px; color: #f4eee4; } + .home.ask button { flex: 1; padding: 10px; } + .home.ask .keep { background: rgba(255, 255, 255, .1); color: #f4eee4; } + .home.ask .rm { background: #ff453a; color: #fff; } @@ -39,6 +54,7 @@

+
@@ -58,12 +74,19 @@ notfound: "Dashboard Studio staat niet in Home Assistant. Vraag wie Home Assistant beheert om het te installeren.", noaddress: "Dashboard Studio is gevonden, maar het adres kon niet worden opgevraagd. Tik in Dashboard Studio op Openen in Haven, of vraag wie Home Assistant beheert.", error: "Verbinden lukte niet: {0}", + foundTitle: "Op dit netwerk", + looking: "Home Assistant zoeken…", + notFound: "Niet gevonden op dit netwerk.", + or: "Of vul het adres zelf in:", + remove: "Verwijderen", keep: "Bewaren", removeQ: "{0} uit Haven verwijderen?", }; const EN = { offline: "Home Assistant can't be reached at this address. Check the address and your connection.", notfound: "Dashboard Studio isn't installed in Home Assistant. Ask whoever manages Home Assistant to install it.", noaddress: "Dashboard Studio was found, but its address couldn't be looked up. Tap Open in Haven in Dashboard Studio, or ask whoever manages Home Assistant.", error: "Couldn't connect: {0}", + notFound: "None found on this network.", + remove: "Remove", keep: "Keep", removeQ: "Remove {0} from Haven?", }; const T = navigator.language.startsWith("nl") ? NL : EN; if (T === NL) document.querySelectorAll("[data-t]").forEach((el) => { el.textContent = NL[el.dataset.t] || el.textContent; }); @@ -80,13 +103,40 @@ const removeLabel = T === NL ? "Verwijderen" : "Remove"; box.innerHTML = homes.map((h) => `
`).join(""); + const words = (k) => (T[k] || EN[k] || k); box.onclick = (e) => { - const go = e.target.closest("[data-id]"), x = e.target.closest("[data-x]"); + const go = e.target.closest("[data-id]"), x = e.target.closest("[data-x]"), b = e.target.closest("[data-rm], [data-keep]"); if (go) app.switchHome(go.dataset.id); - if (x && confirm((T === NL ? "Verwijderen: " : "Remove ") + x.closest(".home").querySelector(".go").firstChild.textContent + "?")) app.removeHome(x.dataset.x); + // removing asks first, right there in the list + if (x) { + const row = x.closest(".home"), name = row.querySelector(".go").firstChild.textContent; + row.classList.add("ask"); + row.innerHTML = `

${esc(words("removeQ").replace("{0}", name))}

`; + } + if (b?.dataset.rm) app.removeHome(b.dataset.rm); + if (b && "keep" in b.dataset) location.reload(); }; - if (homes.length && homes.some((h) => h.current) && !why) { form.classList.add("hide"); add.hidden = false; } + const adding = !(homes.length && homes.some((h) => h.current) && !why); + if (!adding) { form.classList.add("hide"); add.hidden = false; } add.onclick = () => app.addHome(); + + // Home Assistant announces itself on the home network: list what is found, to tap instead of typing. + if (adding && app && app.findHomes) { + const wrap = document.getElementById("found"), list = document.getElementById("foundList"), look = document.getElementById("look"); + const known = new Set(homes.map((h) => h.ha.toLowerCase())); + app.findHomes(); + wrap.hidden = false; + let n = 0; + const tick = () => { + const found = JSON.parse(app.found()).filter((f) => !known.has(f.url.toLowerCase())); + list.innerHTML = found.map((f) => ``).join(""); + if (++n < 30) setTimeout(tick, 1000); + else { look.classList.add("done"); if (!found.length) look.textContent = words("notFound"); else look.hidden = true; } + if (found.length) look.hidden = true; + }; + tick(); + list.onclick = (e) => { const b = e.target.closest("[data-url]"); if (b) app.setup(b.dataset.url); }; + } diff --git a/haven/src/nl/melvanveen/haven/MainActivity.java b/haven/src/nl/melvanveen/haven/MainActivity.java index 5d30473..4295519 100644 --- a/haven/src/nl/melvanveen/haven/MainActivity.java +++ b/haven/src/nl/melvanveen/haven/MainActivity.java @@ -12,10 +12,15 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.graphics.Color; import android.net.Uri; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; +import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.view.WindowManager; import android.webkit.CookieManager; @@ -28,11 +33,18 @@ import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayDeque; +import java.util.Locale; +import java.util.Map; + /** * Haven: the dashboards of Dashboard Studio as an Android app, for everyone in the home. Full screen, and only * the dashboards: Dashboard Studio leaves out its builder and admin screen when window.HavenApp is there. @@ -49,6 +61,9 @@ public class MainActivity extends Activity { private WebView web; private SharedPreferences prefs; private String message = "", detail = ""; + private String tried = ""; // the address last tried, to fill in again when it didn't work + private TextView status, another; // on the loading screen: what Haven is doing, and a way out + private static final boolean NL = Locale.getDefault().getLanguage().equals("nl"); private long connectedAt = 0; private ValueCallback fileCallback; @@ -101,7 +116,28 @@ public class MainActivity extends Activity { web = new WebView(this); web.setBackgroundColor(Color.TRANSPARENT); // the loading screen shows through until a page is ready - setContentView(web); + FrameLayout root = new FrameLayout(this); + root.addView(web); + // Under the icon of the loading screen: what Haven is doing, and after a while a way to choose another home. + LinearLayout box = new LinearLayout(this); + box.setOrientation(LinearLayout.VERTICAL); + box.setGravity(Gravity.CENTER_HORIZONTAL); + box.setPadding(dp(24), dp(130), dp(24), 0); + status = new TextView(this); + status.setTextColor(Color.parseColor("#a9b8c2")); + status.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + status.setGravity(Gravity.CENTER); + another = new TextView(this); + another.setText(NL ? "Ander huis of adres kiezen" : "Choose another home or address"); + another.setTextColor(Color.parseColor("#f4a531")); + another.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + another.setPadding(dp(16), dp(20), dp(16), dp(12)); + another.setOnClickListener((v) -> showSetup("", "")); + box.addView(status); + box.addView(another); + root.addView(box, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); + showStatus(false); + setContentView(root); WebSettings s = web.getSettings(); s.setJavaScriptEnabled(true); s.setDomStorageEnabled(true); @@ -140,7 +176,8 @@ public class MainActivity extends Activity { private void save(String ha, String app) { try { JSONObject h = Hass.current(this); - if (h == null || !h.optString("ha").equalsIgnoreCase(ha)) h = new JSONObject().put("ha", ha); + // a new address is only kept for good once it has connected (see Bridge.open) + if (h == null || !h.optString("ha").equalsIgnoreCase(ha)) h = new JSONObject().put("ha", ha).put("pending", "true"); h.put("app", app); Hass.setCurrent(this, Hass.save(this, h).optString("id")); } catch (JSONException ignored) { /* nothing to save */ } @@ -168,15 +205,35 @@ public class MainActivity extends Activity { /** While Home Assistant's page does its work, the loading screen (the window background) shows instead. */ private void hidePage() { web.setAlpha(0f); + showStatus(true); reveal.removeCallbacks(showPage); reveal.postDelayed(showPage, 6000); // still there after a while: probably something to read or answer } private final android.os.Handler reveal = new android.os.Handler(android.os.Looper.getMainLooper()); - private final Runnable showPage = () -> web.animate().alpha(1f).setDuration(150); + private final Runnable showPage = () -> { showStatus(false); web.animate().alpha(1f).setDuration(150); }; + private final Runnable offerAnother = () -> another.setVisibility(View.VISIBLE); + + /** "Connecting to Home…" on the loading screen; after a while also "Choose another home or address". */ + private void showStatus(boolean on) { + reveal.removeCallbacks(offerAnother); + another.setVisibility(View.GONE); + if (!on) { status.setVisibility(View.GONE); return; } + JSONObject h = Hass.current(this); + String name = h == null || h.optString("pending").equals("true") ? "" : h.optString("name"); + status.setText(name.isEmpty() ? (NL ? "Verbinden met Home Assistant…" : "Connecting to Home Assistant…") : (NL ? "Verbinden met " : "Connecting to ") + name + "…"); + status.setVisibility(View.VISIBLE); + reveal.postDelayed(offerAnother, 12000); + } + + private int dp(int v) { return Math.round(v * getResources().getDisplayMetrics().density); } private boolean offlineTried = false; private void showSetup(String why, String more) { message = why; detail = more == null ? "" : more; + showStatus(false); + // An address that never connected isn't kept as a home; it is filled in again to correct it. + JSONObject h = Hass.current(this); + if (h != null && h.optString("pending").equals("true")) { tried = h.optString("ha"); Hass.remove(this, h.optString("id")); } web.loadUrl(SETUP); } @@ -185,12 +242,12 @@ public class MainActivity extends Activity { public void onPageStarted(WebView view, String url, android.graphics.Bitmap icon) { boolean haPage = url != null && !ha().isEmpty() && url.startsWith(ha()) && !isIngress(url) && !url.equals(app().isEmpty() ? "-" : app()); if (haPage && !url.contains("/auth/")) hidePage(); - else if (url != null && url.contains("/auth/")) { reveal.removeCallbacks(showPage); web.setAlpha(1f); } // signing in + else if (url != null && url.contains("/auth/")) { reveal.removeCallbacks(showPage); showStatus(false); web.setAlpha(1f); } // signing in } @Override public void onPageFinished(WebView view, String url) { - if (url != null && (isIngress(url) || url.startsWith("file:"))) { reveal.removeCallbacks(showPage); web.animate().alpha(1f).setDuration(150); } + if (url != null && (isIngress(url) || url.startsWith("file:"))) { reveal.removeCallbacks(showPage); showStatus(false); web.animate().alpha(1f).setDuration(150); } if (url == null || ha().isEmpty() || !url.startsWith(ha())) return; String page = app().replaceAll("[?#].*", ""); if (isIngress(url) || (!page.isEmpty() && url.startsWith(page))) { @@ -216,7 +273,7 @@ public class MainActivity extends Activity { if (!req.isForMainFrame() || req.getUrl().toString().startsWith("file:")) return; // Home Assistant can't be reached: Dashboard Studio's offline copy may still open, with the last known states. if (!isIngress(req.getUrl().toString()) && !app().isEmpty() && !offlineTried) { offlineTried = true; web.loadUrl(app()); } - else { reveal.removeCallbacks(showPage); web.setAlpha(1f); showSetup("offline", ""); } + else { reveal.removeCallbacks(showPage); showStatus(false); web.setAlpha(1f); showSetup("offline", ""); } } @Override @@ -261,8 +318,12 @@ public class MainActivity extends Activity { @Override public void onBackPressed() { - if (web.canGoBack()) web.goBack(); - else super.onBackPressed(); + // The page first: back closes an open sheet or menu there. Then its history; then Haven goes to the background. + web.evaluateJavascript("(window.havenBack && window.havenBack()) ? 1 : 0", (v) -> { + if ("1".equals(v)) return; + if (web.canGoBack()) web.goBack(); + else moveTaskToBack(true); + }); } @Override @@ -340,6 +401,90 @@ public class MainActivity extends Activity { } catch (ActivityNotFoundException ignored) { /* no installer */ } } + // ---------------------------------------------------------------- finding Home Assistant + // Home Assistant announces itself on the home network (zeroconf, "_home-assistant._tcp"), with its name and + // address. The setup page lists what is found, so nobody has to know the address. + private NsdManager nsd; + private NsdManager.DiscoveryListener finder; + private WifiManager.MulticastLock multicast; + private final JSONArray found = new JSONArray(); // {name, url} + private final ArrayDeque toResolve = new ArrayDeque<>(); + private boolean resolving = false; + private final Runnable stopFinding = this::stopFinding; + + private void startFinding() { + if (finder != null) return; + nsd = (NsdManager) getSystemService(NSD_SERVICE); + if (nsd == null) return; + try { + multicast = ((WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE)).createMulticastLock("haven"); + multicast.setReferenceCounted(false); + multicast.acquire(); + } catch (Exception ignored) { multicast = null; /* finding may still work */ } + finder = new NsdManager.DiscoveryListener() { + @Override public void onServiceFound(NsdServiceInfo info) { synchronized (toResolve) { toResolve.add(info); } resolveNext(); } + @Override public void onServiceLost(NsdServiceInfo info) {} + @Override public void onDiscoveryStarted(String type) {} + @Override public void onDiscoveryStopped(String type) {} + @Override public void onStartDiscoveryFailed(String type, int code) { finder = null; } + @Override public void onStopDiscoveryFailed(String type, int code) {} + }; + try { nsd.discoverServices("_home-assistant._tcp", NsdManager.PROTOCOL_DNS_SD, finder); } catch (Exception e) { finder = null; } + reveal.postDelayed(stopFinding, 60000); + } + + private void stopFinding() { + reveal.removeCallbacks(stopFinding); + if (finder != null) try { nsd.stopServiceDiscovery(finder); } catch (Exception ignored) { /* already stopped */ } + finder = null; + if (multicast != null) try { multicast.release(); } catch (Exception ignored) { /* not held */ } + multicast = null; + } + + /** One at a time: older Android resolves only one service at once. */ + private void resolveNext() { + NsdServiceInfo next; + synchronized (toResolve) { + if (resolving || toResolve.isEmpty()) return; + resolving = true; + next = toResolve.poll(); + } + nsd.resolveService(next, new NsdManager.ResolveListener() { + @Override public void onResolveFailed(NsdServiceInfo info, int code) { synchronized (toResolve) { resolving = false; } resolveNext(); } + @Override public void onServiceResolved(NsdServiceInfo info) { addFound(info); synchronized (toResolve) { resolving = false; } resolveNext(); } + }); + } + + private void addFound(NsdServiceInfo info) { + Map txt = info.getAttributes(); + String url = text(txt, "internal_url"); + if (url.isEmpty()) url = text(txt, "base_url"); + if (url.isEmpty() && info.getHost() != null) { + String host = info.getHost().getHostAddress(); + if (host == null) return; + if (host.contains(":")) host = "[" + host.replaceAll("%.*", "") + "]"; // IPv6 + url = "http://" + host + ":" + info.getPort(); + } + if (!url.matches("(?i)https?://.+")) return; + url = url.replaceAll("/+$", ""); + String name = text(txt, "location_name"); + if (name.isEmpty()) name = info.getServiceName(); + try { + synchronized (found) { + for (int i = 0; i < found.length(); i++) if (found.getJSONObject(i).optString("url").equalsIgnoreCase(url)) return; + found.put(new JSONObject().put("name", name).put("url", url)); + } + } catch (JSONException ignored) { /* skip this one */ } + } + + private static String text(Map txt, String key) { + byte[] v = txt == null ? null : txt.get(key); + return v == null ? "" : new String(v, java.nio.charset.StandardCharsets.UTF_8).trim(); + } + + @Override + protected void onDestroy() { stopFinding(); super.onDestroy(); } + @Override protected void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); web.saveState(out); } @@ -351,7 +496,7 @@ public class MainActivity extends Activity { /** What the pages can call, as window.HavenApp. */ private class Bridge { - @JavascriptInterface public String address() { return ha(); } + @JavascriptInterface public String address() { return tried.isEmpty() ? ha() : tried; } @JavascriptInterface public String appUrl() { return app(); } @JavascriptInterface public String message() { return message; } @JavascriptInterface public String detail() { return detail; } @@ -364,6 +509,7 @@ public class MainActivity extends Activity { if (!text.matches("(?i)https?://.*")) text = "http://" + text; Uri u = Uri.parse(text); if (u.getHost() == null || u.getHost().isEmpty()) { showSetup("offline", ""); return; } + stopFinding(); save(origin(u), isIngress(text) ? text : ""); start(); }); @@ -373,6 +519,9 @@ public class MainActivity extends Activity { runOnUiThread(() -> { if (url == null || !url.startsWith(ha())) return; save(ha(), url); + JSONObject h = Hass.current(MainActivity.this); + if (h != null && h.optString("pending").equals("true")) { try { Hass.save(MainActivity.this, h.put("pending", "false")); } catch (JSONException ignored) { /* kept as it was */ } } // it connected: keep it + tried = ""; web.loadUrl(url); }); } @@ -401,12 +550,17 @@ public class MainActivity extends Activity { try { for (int i = 0; i < list.length(); i++) { JSONObject h = list.getJSONObject(i); + if (h.optString("pending").equals("true")) continue; out.put(new JSONObject().put("id", h.optString("id")).put("name", h.optString("name")).put("ha", h.optString("ha")).put("current", cur.equals(h.optString("id")))); } } catch (JSONException ignored) { /* an empty list */ } return out.toString(); } + /** The setup page: look for Home Assistant on this network, and what was found so far. */ + @JavascriptInterface public void findHomes() { runOnUiThread(MainActivity.this::startFinding); } + @JavascriptInterface public String found() { synchronized (found) { return found.toString(); } } + @JavascriptInterface public void switchHome(String id) { runOnUiThread(() -> { Hass.setCurrent(MainActivity.this, id); start(); }); } @JavascriptInterface public void addHome() { runOnUiThread(() -> { Hass.setCurrent(MainActivity.this, ""); showSetup("", ""); }); }