Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
7ad67f4
Carry over Pascal Home Assistant work onto dev-ha
Niutels Apr 22, 2026
be837e4
Merge remote-tracking branch 'origin/main' into dev-ha
Niutels Apr 26, 2026
b706692
Add Home Assistant smart home controls
Niutels Apr 28, 2026
fd20d45
Remove Home Assistant debug logging route
Niutels Apr 28, 2026
8194220
Merge origin/main into dev-ha
Niutels Apr 28, 2026
0641ab2
Drop local walkthrough fixes
Niutels Apr 28, 2026
67e1be1
Limit smart home picker to Home Assistant
Niutels Apr 28, 2026
9e25152
Remove Pascal-seeded HA demo bindings
Niutels Apr 28, 2026
9c31130
Keep HA-fed living script bindings
Niutels Apr 28, 2026
364f851
Restructure smart home composition ownership
Niutels Apr 28, 2026
ad35aa2
Finish smart home ownership cleanup
Niutels Apr 28, 2026
63ba7ef
Move smart room overlay out of viewer
Niutels Apr 28, 2026
b461131
Persist user-managed smart room composition
Niutels Apr 28, 2026
b74c588
Prune unrelated smart home PR changes
Niutels Apr 29, 2026
fc5c04e
Erase unrelated drop-list diffs
Niutels Apr 29, 2026
53e9f4e
Make editor config portable across WSL
Niutels Apr 29, 2026
6d488ba
Merge origin/main into dev-ha
Niutels Apr 29, 2026
9e594ee
Make HA connectivity refresh explicit
Niutels Apr 29, 2026
1828c73
Persist Home Assistant pill edits
Niutels Apr 29, 2026
0989212
Reduce Home Assistant PR footprint
Niutels Apr 29, 2026
0ab4fd4
Isolate Home Assistant scene hydration
Niutels Apr 29, 2026
da2babf
Extract Home Assistant resource grouping helpers
Niutels Apr 29, 2026
7c729ff
Extract Home Assistant room control model
Niutels Apr 29, 2026
5a509e9
Extract Home Assistant panel layout helpers
Niutels Apr 29, 2026
d47c943
Extract Home Assistant room overlay builder
Niutels Apr 29, 2026
e9e2c59
Generalize collection attachment cloning
Niutels Apr 29, 2026
137ac30
Centralize immediate scene save event
Niutels Apr 29, 2026
a6fd6b8
Move Home Assistant panel category helpers
Niutels Apr 29, 2026
d0ac308
Move room control display helpers
Niutels Apr 29, 2026
f308a4d
Move Home Assistant collection helpers
Niutels Apr 29, 2026
bfebc80
Extract Home Assistant pill mutations
Niutels Apr 29, 2026
d92c2f0
Move Home Assistant panel layout derivations
Niutels Apr 29, 2026
f4bdf92
Remove stale viewer portal dependency
Niutels Apr 29, 2026
cb6d61d
Drop core barrel churn
Niutels Apr 29, 2026
464ecd7
Limit schema barrel diff
Niutels Apr 29, 2026
8a26753
Centralize collection attachment helpers
Niutels Apr 29, 2026
492f5a7
Reuse Home Assistant pill height constant
Niutels Apr 29, 2026
2cbfe16
fix HA room pill refresh preservation
Niutels Apr 29, 2026
8f71fcb
Merge remote-tracking branch 'origin/main' into dev-ha
Niutels Apr 30, 2026
9207bd0
Preserve Home Assistant bindings and floor-aware controls
Niutels Apr 30, 2026
1a284aa
Fit Smart Home groups section to content
Niutels Apr 30, 2026
1d1c0b8
Fit Smart Home device categories to content
Niutels Apr 30, 2026
3f36ae0
Guard viewer interactive system during scene resets
Niutels May 1, 2026
d4be959
Revert "Guard viewer interactive system during scene resets"
Niutels May 1, 2026
b56c27a
Remove unrelated shared stack cleanup
Niutels May 1, 2026
8f03382
Smooth Smart Home panel resizing
Niutels May 1, 2026
75a7fa4
Improve Home Assistant LAN discovery
Niutels May 2, 2026
2203641
Refine Home Assistant connection and controls
Niutels May 3, 2026
aed3974
Add Home Assistant demo GIF
Niutels May 3, 2026
07c64b3
Remove local default layout path
Niutels May 3, 2026
d4aade0
Remove local debug ignore entries
Niutels May 3, 2026
571779f
Use normal editor scene loading
Niutels May 3, 2026
2026068
Add Lovelace viewer card publishing
Niutels May 4, 2026
a5e3618
Make Lovelace card HACS-ready
Niutels May 4, 2026
327eef3
Extract Home Assistant runtime package
Niutels May 4, 2026
032c65b
Tighten Lovelace runtime packaging
Niutels May 4, 2026
b79b7ed
Fix Lovelace R3F context sharing
Niutels May 4, 2026
989224c
Fix Lovelace RTS hit targets
Niutels May 4, 2026
47ee486
Fix Lovelace grouped pill dispatch
Niutels May 4, 2026
2ebf2f5
Suppress stale HA state after Lovelace actions
Niutels May 4, 2026
ecaf8b6
Fix Lovelace pill edge visibility
Niutels May 4, 2026
f82bb3f
Hide Lovelace pills at viewport edges
Niutels May 4, 2026
01a6d64
Consolidate Home Assistant feature package
Niutels May 4, 2026
eeaa52f
Refactor Home Assistant integration package
Niutels May 5, 2026
3ec71a9
Prepare Lovelace HACS integration
Niutels May 5, 2026
73e8a07
Refine Home Assistant Lovelace controls
Niutels May 5, 2026
cdc3bcc
Prepare Lovelace card for HACS install flow
Niutels May 6, 2026
6b4586e
Merge remote-tracking branch 'origin/main' into dev-lovelace
Niutels May 6, 2026
91ff3a1
Restore main asset URL localhost allowance
Niutels May 6, 2026
b6a2573
Fix Home Assistant TV screen glow transform
Niutels May 6, 2026
1cd947c
Fix Lovelace TV glow without R3F portal
Niutels May 6, 2026
4f75e28
Fix smart home overlay binding repair
Niutels May 7, 2026
9c3a691
Fix smart home device grouping for HACS card
Niutels May 7, 2026
43182a1
Add Home Assistant Lovelace embed screenshot
Niutels May 7, 2026
0df180e
Merge origin main into Lovelace branch
Niutels May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/hacs-validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Validate HACS

on:
push:
pull_request:
workflow_dispatch:

permissions: {}

jobs:
validate-hacs:
runs-on: ubuntu-latest
steps:
- name: HACS validation
uses: hacs/action@main
with:
category: plugin
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package-lock.json

# Testing
coverage
test-results/

# Turbo
.turbo
Expand All @@ -30,7 +31,9 @@ supabase/.temp/
.next/
out/
build
dist
**/dist/
!/dist/
!/dist/pascal-viewer-card.js
*.tsbuildinfo


Expand Down
32 changes: 32 additions & 0 deletions apps/editor/app/api/home-assistant/connect/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
resolveHomeAssistantServerConfig,
validateHomeAssistantConnection,
} from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

export async function GET() {
try {
const result = await validateHomeAssistantConnection(await resolveHomeAssistantServerConfig())
return Response.json(result, { status: result.success ? 200 : 200 })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to connect to Home Assistant.'
return Response.json(
{
baseUrl: null,
castEntityId: null,
castFriendlyName: null,
clientId: null,
entityCount: 0,
error: message,
externalUrl: null,
instanceUrl: null,
linked: false,
message,
mode: 'unlinked',
success: false,
},
{ status: 500 },
)
}
}
32 changes: 32 additions & 0 deletions apps/editor/app/api/home-assistant/connection-status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
resolveHomeAssistantServerConfig,
validateHomeAssistantConnection,
} from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

export async function GET() {
try {
const result = await validateHomeAssistantConnection(await resolveHomeAssistantServerConfig())
return Response.json(result, { status: result.success ? 200 : 200 })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to connect to Home Assistant.'
return Response.json(
{
baseUrl: null,
castEntityId: null,
castFriendlyName: null,
clientId: null,
entityCount: 0,
error: message,
externalUrl: null,
instanceUrl: null,
linked: false,
message,
mode: 'unlinked',
success: false,
},
{ status: 200 },
)
}
}
70 changes: 70 additions & 0 deletions apps/editor/app/api/home-assistant/device-action/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type {
HomeAssistantActionRequest,
HomeAssistantCollectionBinding,
} from '@pascal-app/home-assistant'
import { getHomeAssistantLink } from '@pascal-app/home-assistant'
import {
resolveHomeAssistantServerConfig,
runHomeAssistantCollectionAction,
runHomeAssistantDeviceAction,
} from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

type DeviceActionRequestBody = {
binding?: HomeAssistantCollectionBinding
collectionName?: string
itemName?: string
link?: unknown
request?: HomeAssistantActionRequest
}

export async function POST(request: Request) {
try {
const body = (await request.json()) as DeviceActionRequestBody
if (
body.binding &&
typeof body.binding === 'object' &&
body.request &&
typeof body.request === 'object'
) {
const collectionName =
typeof body.collectionName === 'string' && body.collectionName.trim().length > 0
? body.collectionName.trim()
: 'Linked collection'

const result = await runHomeAssistantCollectionAction(
await resolveHomeAssistantServerConfig(),
collectionName,
body.binding,
body.request,
)
return Response.json(result)
}

const itemName =
typeof body.itemName === 'string' && body.itemName.trim().length > 0
? body.itemName.trim()
: 'Linked item'
const link = getHomeAssistantLink({
homeAssistantLink: body.link,
})

if (!link) {
return Response.json(
{ error: 'Missing or invalid Home Assistant link payload.' },
{ status: 400 },
)
}

const result = await runHomeAssistantDeviceAction(
await resolveHomeAssistantServerConfig(),
itemName,
link,
)
return Response.json(result)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown Home Assistant action error.'
return Response.json({ error: message }, { status: 500 })
}
}
38 changes: 38 additions & 0 deletions apps/editor/app/api/home-assistant/discover-devices/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { discoverHomeAssistantDevices } from '@pascal-app/home-assistant/server'
import {
hasHomeAssistantServerConfig,
resolveHomeAssistantServerConfig,
} from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

export async function GET() {
try {
const config = await resolveHomeAssistantServerConfig()
if (!hasHomeAssistantServerConfig(config)) {
return Response.json(
{
devices: [],
error: 'Home Assistant is not linked yet.',
},
{ status: 412 },
)
}

const devices = await discoverHomeAssistantDevices(config)
return Response.json({
devices,
scannedAt: new Date().toISOString(),
})
} catch (error) {
const message =
error instanceof Error ? error.message : 'Unknown Home Assistant discovery error.'
return Response.json(
{
devices: [],
error: message,
},
{ status: 500 },
)
}
}
24 changes: 24 additions & 0 deletions apps/editor/app/api/home-assistant/discover-instances/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { discoverHomeAssistantInstances } from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

export async function GET() {
try {
const instances = await discoverHomeAssistantInstances()
return Response.json({
instances,
scannedAt: new Date().toISOString(),
})
} catch (error) {
const message =
error instanceof Error ? error.message : 'Unknown Home Assistant discovery error.'

return Response.json(
{
error: message,
instances: [],
},
{ status: 500 },
)
}
}
37 changes: 37 additions & 0 deletions apps/editor/app/api/home-assistant/import-resources/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { listImportableHomeAssistantResources } from '@pascal-app/home-assistant/server'
import {
hasHomeAssistantServerConfig,
resolveHomeAssistantServerConfig,
} from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

export async function GET() {
try {
const config = await resolveHomeAssistantServerConfig()
if (!hasHomeAssistantServerConfig(config)) {
return Response.json(
{
error: 'Home Assistant is not linked yet.',
resources: [],
},
{ status: 412 },
)
}

const resources = await listImportableHomeAssistantResources(config)
return Response.json({
importedAt: new Date().toISOString(),
resources,
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown Home Assistant import error.'
return Response.json(
{
error: message,
resources: [],
},
{ status: 500 },
)
}
}
81 changes: 81 additions & 0 deletions apps/editor/app/api/home-assistant/oauth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import {
exchangeAuthorizationCode,
HOME_ASSISTANT_OAUTH_COOKIE,
} from '@pascal-app/home-assistant/server'
import { writeLinkedHomeAssistantProfile } from '@pascal-app/home-assistant/server'

export const runtime = 'nodejs'

function buildRedirectUrl(base: string, status: 'success' | 'error', message?: string) {
const redirectUrl = new URL('/', base)
redirectUrl.searchParams.set('ha_link', status)
if (message) {
redirectUrl.searchParams.set('ha_message', message)
}
return redirectUrl
}

export async function GET(request: NextRequest) {
const oauthCookie = request.cookies.get(HOME_ASSISTANT_OAUTH_COOKIE)?.value
const fallbackBase = request.nextUrl.origin

if (!oauthCookie) {
return NextResponse.redirect(
buildRedirectUrl(fallbackBase, 'error', 'Missing Home Assistant OAuth state.'),
)
}

try {
const oauthState = JSON.parse(oauthCookie) as {
clientId?: string
externalUrl?: string | null
instanceUrl?: string
state?: string
}
const code = request.nextUrl.searchParams.get('code')
const state = request.nextUrl.searchParams.get('state')

if (!(oauthState.clientId && oauthState.instanceUrl && oauthState.state && code && state)) {
throw new Error('Missing OAuth callback parameters.')
}

if (state !== oauthState.state) {
throw new Error('Home Assistant OAuth state did not match.')
}

const tokens = await exchangeAuthorizationCode(
oauthState.instanceUrl,
oauthState.clientId,
code,
oauthState.externalUrl,
)

await writeLinkedHomeAssistantProfile({
accessToken: tokens.access_token,
accessTokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1000).toISOString(),
clientId: oauthState.clientId,
externalUrl:
typeof oauthState.externalUrl === 'string' && oauthState.externalUrl.trim().length > 0
? oauthState.externalUrl
: null,
instanceUrl: oauthState.instanceUrl,
linkedAt: new Date().toISOString(),
refreshToken: tokens.refresh_token ?? '',
})

const response = NextResponse.redirect(buildRedirectUrl(oauthState.clientId, 'success'))
response.cookies.delete(HOME_ASSISTANT_OAUTH_COOKIE)
return response
} catch (error) {
const message =
error instanceof Error ? error.message : 'Failed to complete Home Assistant sign-in.'
const parsedCookie = JSON.parse(oauthCookie) as { clientId?: string }
const response = NextResponse.redirect(
buildRedirectUrl(parsedCookie.clientId ?? fallbackBase, 'error', message),
)
response.cookies.delete(HOME_ASSISTANT_OAUTH_COOKIE)
return response
}
}
Loading