feat: initial smart home dashboard
All checks were successful
Deploy to Coolify / deploy (push) Successful in 44s

- Next.js 14 + TypeScript + Tailwind CSS
- Glassmorphism design with ambient orbs
- Cards: Light x2, Temperature, AirPurifier, Tasks, Weather, Savings
- Home Assistant integration (demo mode if no token)
- Vikunja tasks API
- Pulse savings API
- wttr.in weather
- Framer Motion animations
- Dark/light theme toggle
- Bottom navigation
- Dockerfile for deployment
This commit is contained in:
Cosmo
2026-04-22 10:00:41 +00:00
commit 9044869fa4
29 changed files with 2439 additions and 0 deletions

97
app/api/ha/route.ts Normal file
View File

@@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from "next/server";
const HA_URL = process.env.HA_URL || "http://192.168.31.110:8123";
const HA_TOKEN = process.env.HA_TOKEN || "";
// Mock data for demo mode
const MOCK_STATES = {
"light.living_room": {
entity_id: "light.living_room",
state: "on",
attributes: { brightness: 180, friendly_name: "Свет Гостиная" },
},
"light.bedroom": {
entity_id: "light.bedroom",
state: "off",
attributes: { friendly_name: "Свет Спальня" },
},
"climate.thermostat": {
entity_id: "climate.thermostat",
state: "heat",
attributes: {
current_temperature: 21.5,
temperature: 22,
friendly_name: "Термостат",
},
},
"fan.air_purifier": {
entity_id: "fan.air_purifier",
state: "on",
attributes: {
preset_mode: "Auto",
friendly_name: "Очиститель воздуха",
preset_modes: ["Auto", "Night", "High"],
},
},
};
export async function GET(req: NextRequest) {
if (!HA_TOKEN) {
return NextResponse.json({ demo: true, states: MOCK_STATES });
}
try {
const res = await fetch(`${HA_URL}/api/states`, {
headers: {
Authorization: `Bearer ${HA_TOKEN}`,
"Content-Type": "application/json",
},
next: { revalidate: 0 },
});
if (!res.ok) throw new Error(`HA responded ${res.status}`);
const states = await res.json();
const relevant = [
"light.living_room",
"light.bedroom",
"climate.thermostat",
"fan.air_purifier",
];
const filtered: Record<string, any> = {};
for (const s of states) {
if (relevant.includes(s.entity_id)) {
filtered[s.entity_id] = s;
}
}
return NextResponse.json({ demo: false, states: filtered });
} catch (e) {
return NextResponse.json({ demo: true, states: MOCK_STATES });
}
}
export async function POST(req: NextRequest) {
const { domain, service, entity_id, ...serviceData } = await req.json();
if (!HA_TOKEN) {
return NextResponse.json({ success: true, demo: true });
}
try {
const res = await fetch(`${HA_URL}/api/services/${domain}/${service}`, {
method: "POST",
headers: {
Authorization: `Bearer ${HA_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ entity_id, ...serviceData }),
});
if (!res.ok) throw new Error(`HA responded ${res.status}`);
return NextResponse.json({ success: true });
} catch (e) {
return NextResponse.json({ success: false, error: String(e) }, { status: 500 });
}
}