Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
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
1 change: 1 addition & 0 deletions .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 Down
150 changes: 150 additions & 0 deletions apps/editor/app/_lib/home-assistant-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { randomUUID } from 'node:crypto'
import type { NextRequest } from 'next/server'

export const HOME_ASSISTANT_OAUTH_COOKIE = 'pascal_ha_oauth'

export type HomeAssistantOauthCookieState = {
clientId: string
externalUrl: string | null
instanceUrl: string
redirectUri: string
state: string
}

export type HomeAssistantTokenResponse = {
access_token: string
expires_in: number
refresh_token?: string
token_type: string
}

function normalizeUrlValue(value: string) {
return value.trim().replace(/\/$/, '')
}

export function normalizeHomeAssistantUrl(value: string) {
const normalized = normalizeUrlValue(value)
const url = new URL(normalized)
if (!(url.protocol === 'http:' || url.protocol === 'https:')) {
throw new Error('Home Assistant URL must use http or https.')
}
return url.toString().replace(/\/$/, '')
}

export function normalizeOptionalHomeAssistantUrl(value: string | null | undefined) {
if (!value || value.trim().length === 0) {
return null
}
return normalizeHomeAssistantUrl(value)
}

export function getRequestOrigin(request: NextRequest) {
const forwardedHost = request.headers.get('x-forwarded-host')
const forwardedProto = request.headers.get('x-forwarded-proto')
if (forwardedHost && forwardedProto) {
return `${forwardedProto}://${forwardedHost}`
}
return request.nextUrl.origin
}

export function buildHomeAssistantOauthState(
request: NextRequest,
instanceUrl: string,
externalUrl: string | null,
): HomeAssistantOauthCookieState {
const clientId = getRequestOrigin(request)
return {
clientId,
externalUrl,
instanceUrl,
redirectUri: `${clientId}/api/home-assistant/oauth/callback`,
state: randomUUID(),
}
}

function getOauthBaseUrl(
oauthState: Pick<HomeAssistantOauthCookieState, 'externalUrl' | 'instanceUrl'>,
) {
return oauthState.externalUrl ?? oauthState.instanceUrl
}

export function buildHomeAssistantAuthorizeUrl(oauthState: HomeAssistantOauthCookieState) {
const authorizeUrl = new URL('/auth/authorize', getOauthBaseUrl(oauthState))
authorizeUrl.searchParams.set('client_id', oauthState.clientId)
authorizeUrl.searchParams.set('redirect_uri', oauthState.redirectUri)
authorizeUrl.searchParams.set('state', oauthState.state)
return authorizeUrl.toString()
}

function buildTokenRequestBody(params: Record<string, string>) {
const body = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
body.set(key, value)
}
return body
}

async function readTokenResponse(response: Response) {
const payload = (await response.json()) as
| HomeAssistantTokenResponse
| {
error?: string
error_description?: string
}

if (!response.ok) {
const errorPayload = 'access_token' in payload ? null : payload
throw new Error(
errorPayload?.error_description ||
errorPayload?.error ||
'Home Assistant token request failed.',
)
}

return payload as HomeAssistantTokenResponse
}

export async function exchangeAuthorizationCode(
instanceUrl: string,
clientId: string,
code: string,
externalUrl?: string | null,
) {
const tokenUrl = new URL('/auth/token', externalUrl ?? instanceUrl)
const response = await fetch(tokenUrl, {
body: buildTokenRequestBody({
client_id: clientId,
code,
grant_type: 'authorization_code',
}),
cache: 'no-store',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
})

return readTokenResponse(response)
}

export async function refreshHomeAssistantAccessToken(
instanceUrl: string,
clientId: string,
refreshToken: string,
) {
const tokenUrl = new URL('/auth/token', instanceUrl)
const response = await fetch(tokenUrl, {
body: buildTokenRequestBody({
client_id: clientId,
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
cache: 'no-store',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
})

return readTokenResponse(response)
}
Loading