feat: new layout, rooms row, fix weather+HA, fix BottomNav overflow
All checks were successful
Deploy to Coolify / deploy (push) Successful in 5s
All checks were successful
Deploy to Coolify / deploy (push) Successful in 5s
- Remove TasksCard and SavingsCard from home tab - New grid layout: lights+thermostat row 1, purifier+weather row 2 - Add RoomsRow component with room navigation - Fix HA entity mapping: fan.zhimi_rmb1_9528_air_purifier → fan.air_purifier - Add real entity aliases for HA route - Fix weather route: add timeout, better error handling - Fix BottomNav: use 100dvh + flex-shrink-0 - TopBar: accept isDemo prop, show Demo badge in header - WeatherCard: compact prop, better loading/error states - globals.css: add no-scrollbar utility
This commit is contained in:
@@ -4,12 +4,17 @@ 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 = {
|
||||
// Real entity IDs → normalized keys used in the dashboard
|
||||
const REAL_ENTITY_ALIASES: Record<string, string> = {
|
||||
"fan.zhimi_rmb1_9528_air_purifier": "fan.air_purifier",
|
||||
};
|
||||
|
||||
// Mock data for entities that don't exist yet (квартира строится)
|
||||
const MOCK_MISSING: Record<string, any> = {
|
||||
"light.living_room": {
|
||||
entity_id: "light.living_room",
|
||||
state: "on",
|
||||
attributes: { brightness: 180, friendly_name: "Свет Гостиная" },
|
||||
state: "off",
|
||||
attributes: { brightness: 0, friendly_name: "Свет Гостиная" },
|
||||
},
|
||||
"light.bedroom": {
|
||||
entity_id: "light.bedroom",
|
||||
@@ -18,9 +23,9 @@ const MOCK_STATES = {
|
||||
},
|
||||
"climate.thermostat": {
|
||||
entity_id: "climate.thermostat",
|
||||
state: "heat",
|
||||
state: "off",
|
||||
attributes: {
|
||||
current_temperature: 21.5,
|
||||
current_temperature: null,
|
||||
temperature: 22,
|
||||
friendly_name: "Термостат",
|
||||
},
|
||||
@@ -36,9 +41,16 @@ const MOCK_STATES = {
|
||||
},
|
||||
};
|
||||
|
||||
const RELEVANT_KEYS = [
|
||||
"light.living_room",
|
||||
"light.bedroom",
|
||||
"climate.thermostat",
|
||||
"fan.air_purifier",
|
||||
];
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!HA_TOKEN) {
|
||||
return NextResponse.json({ demo: true, states: MOCK_STATES });
|
||||
return NextResponse.json({ demo: true, states: MOCK_MISSING });
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -51,25 +63,40 @@ export async function GET(req: NextRequest) {
|
||||
});
|
||||
|
||||
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 states: any[] = await res.json();
|
||||
|
||||
// Build filtered map with normalized keys
|
||||
const filtered: Record<string, any> = {};
|
||||
for (const s of states) {
|
||||
if (relevant.includes(s.entity_id)) {
|
||||
// Direct match
|
||||
if (RELEVANT_KEYS.includes(s.entity_id)) {
|
||||
filtered[s.entity_id] = s;
|
||||
}
|
||||
// Alias match (real entity → normalized key)
|
||||
if (REAL_ENTITY_ALIASES[s.entity_id]) {
|
||||
const normalizedKey = REAL_ENTITY_ALIASES[s.entity_id];
|
||||
filtered[normalizedKey] = {
|
||||
...s,
|
||||
entity_id: normalizedKey,
|
||||
_real_entity_id: s.entity_id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ demo: false, states: filtered });
|
||||
// Fill in missing entities with mock data (but mark them)
|
||||
for (const key of RELEVANT_KEYS) {
|
||||
if (!filtered[key]) {
|
||||
filtered[key] = { ...MOCK_MISSING[key], _mock: true };
|
||||
}
|
||||
}
|
||||
|
||||
// demo = true only if ALL entities are mock (no real HA devices connected)
|
||||
const hasAnyReal = RELEVANT_KEYS.some(k => !filtered[k]?._mock);
|
||||
|
||||
return NextResponse.json({ demo: !hasAnyReal, states: filtered });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ demo: true, states: MOCK_STATES });
|
||||
// Fallback to full mock
|
||||
return NextResponse.json({ demo: true, states: MOCK_MISSING });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +107,11 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ success: true, demo: true });
|
||||
}
|
||||
|
||||
// Resolve alias: if normalized key, find real entity
|
||||
const realEntityId = Object.keys(REAL_ENTITY_ALIASES).find(
|
||||
k => REAL_ENTITY_ALIASES[k] === entity_id
|
||||
) || entity_id;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${HA_URL}/api/services/${domain}/${service}`, {
|
||||
method: "POST",
|
||||
@@ -87,7 +119,7 @@ export async function POST(req: NextRequest) {
|
||||
Authorization: `Bearer ${HA_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ entity_id, ...serviceData }),
|
||||
body: JSON.stringify({ entity_id: realEntityId, ...serviceData }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HA responded ${res.status}`);
|
||||
|
||||
@@ -1,27 +1,52 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
function getWeatherCode(desc: string): string {
|
||||
const d = desc.toLowerCase();
|
||||
if (d.includes("overcast")) return "122";
|
||||
if (d.includes("partly cloudy") || d.includes("partly") ) return "116";
|
||||
if (d.includes("cloudy") || d.includes("cloud")) return "119";
|
||||
if (d.includes("drizzle")) return "185";
|
||||
if (d.includes("rain") || d.includes("shower")) return "305";
|
||||
if (d.includes("snow") || d.includes("blizzard")) return "230";
|
||||
if (d.includes("thunder") || d.includes("storm")) return "200";
|
||||
if (d.includes("fog") || d.includes("mist")) return "248";
|
||||
if (d.includes("sunny") || d.includes("clear")) return "113";
|
||||
return "116";
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const res = await fetch(
|
||||
"https://wttr.in/Saint+Petersburg?format=j1",
|
||||
{
|
||||
next: { revalidate: 600 },
|
||||
headers: { "User-Agent": "SmartHomeDashboard/1.0" },
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
"User-Agent": "SmartHomeDashboard/1.0",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error("Weather fetch failed");
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!res.ok) throw new Error(`wttr responded ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
const current = data.current_condition[0];
|
||||
const days = data.weather.slice(0, 3).map((day: any) => {
|
||||
const desc = day.hourly[4]?.weatherDesc?.[0]?.value || "";
|
||||
const days = (data.weather || []).slice(0, 3).map((day: any) => {
|
||||
const hourly = day.hourly?.[4] || day.hourly?.[0] || {};
|
||||
const desc = hourly.weatherDesc?.[0]?.value || "";
|
||||
const code = hourly.weatherCode || getWeatherCode(desc);
|
||||
return {
|
||||
date: day.date,
|
||||
maxTemp: day.maxtempC,
|
||||
minTemp: day.mintempC,
|
||||
desc,
|
||||
weatherCode: day.hourly[4]?.weatherCode || "113",
|
||||
weatherCode: String(code),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -30,20 +55,22 @@ export async function GET() {
|
||||
feelsLike: current.FeelsLikeC,
|
||||
humidity: current.humidity,
|
||||
desc: current.weatherDesc[0]?.value || "",
|
||||
weatherCode: current.weatherCode,
|
||||
weatherCode: String(current.weatherCode),
|
||||
windSpeed: current.windspeedKmph,
|
||||
forecast: days,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Weather fetch error:", e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
temp: "—",
|
||||
feelsLike: "—",
|
||||
humidity: "—",
|
||||
desc: "Нет данных",
|
||||
weatherCode: "113",
|
||||
weatherCode: "116",
|
||||
windSpeed: "—",
|
||||
forecast: [],
|
||||
error: String(e),
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user