fix: weather modal, remove tasks/savings, fix HA controls, safe-area BottomNav
All checks were successful
Deploy to Coolify / deploy (push) Successful in 3s
All checks were successful
Deploy to Coolify / deploy (push) Successful in 3s
This commit is contained in:
@@ -4,22 +4,22 @@ 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 || "";
|
||||
|
||||
// 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: "off",
|
||||
attributes: { brightness: 0, friendly_name: "Свет Гостиная" },
|
||||
_mock: true,
|
||||
},
|
||||
"light.bedroom": {
|
||||
entity_id: "light.bedroom",
|
||||
state: "off",
|
||||
attributes: { friendly_name: "Свет Спальня" },
|
||||
_mock: true,
|
||||
},
|
||||
"climate.thermostat": {
|
||||
entity_id: "climate.thermostat",
|
||||
@@ -29,15 +29,17 @@ const MOCK_MISSING: Record<string, any> = {
|
||||
temperature: 22,
|
||||
friendly_name: "Термостат",
|
||||
},
|
||||
_mock: true,
|
||||
},
|
||||
"fan.air_purifier": {
|
||||
entity_id: "fan.air_purifier",
|
||||
state: "on",
|
||||
state: "off",
|
||||
attributes: {
|
||||
preset_mode: "Auto",
|
||||
friendly_name: "Очиститель воздуха",
|
||||
preset_modes: ["Auto", "Night", "High"],
|
||||
},
|
||||
_mock: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,20 +61,23 @@ export async function GET(req: NextRequest) {
|
||||
Authorization: `Bearer ${HA_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
next: { revalidate: 0 },
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HA responded ${res.status}`);
|
||||
const states: any[] = await res.json();
|
||||
|
||||
// Build filtered map with normalized keys
|
||||
const filtered: Record<string, any> = {};
|
||||
|
||||
// Get real temperature from air purifier sensor
|
||||
const tempSensor = states.find(s => s.entity_id === "sensor.zhimi_rmb1_9528_temperature");
|
||||
const humiditySensor = states.find(s => s.entity_id === "sensor.zhimi_rmb1_9528_relative_humidity");
|
||||
const pm25Sensor = states.find(s => s.entity_id === "sensor.zhimi_rmb1_9528_pm25_density");
|
||||
|
||||
for (const s of states) {
|
||||
// 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] = {
|
||||
@@ -83,31 +88,48 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in missing entities with mock data (but mark them)
|
||||
// Fill missing with mock
|
||||
for (const key of RELEVANT_KEYS) {
|
||||
if (!filtered[key]) {
|
||||
filtered[key] = { ...MOCK_MISSING[key], _mock: true };
|
||||
filtered[key] = { ...MOCK_MISSING[key] };
|
||||
}
|
||||
}
|
||||
|
||||
// demo = true only if ALL entities are mock (no real HA devices connected)
|
||||
// Inject real temperature into thermostat card if sensor exists
|
||||
if (tempSensor && filtered["climate.thermostat"]) {
|
||||
filtered["climate.thermostat"].attributes = {
|
||||
...filtered["climate.thermostat"].attributes,
|
||||
current_temperature: parseFloat(tempSensor.state),
|
||||
humidity: humiditySensor ? parseFloat(humiditySensor.state) : null,
|
||||
pm25: pm25Sensor ? parseFloat(pm25Sensor.state) : null,
|
||||
};
|
||||
}
|
||||
|
||||
const hasAnyReal = RELEVANT_KEYS.some(k => !filtered[k]?._mock);
|
||||
|
||||
return NextResponse.json({ demo: !hasAnyReal, states: filtered });
|
||||
return NextResponse.json({
|
||||
demo: !hasAnyReal,
|
||||
states: filtered,
|
||||
sensors: {
|
||||
temperature: tempSensor ? parseFloat(tempSensor.state) : null,
|
||||
humidity: humiditySensor ? parseFloat(humiditySensor.state) : null,
|
||||
pm25: pm25Sensor ? parseFloat(pm25Sensor.state) : null,
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Fallback to full mock
|
||||
return NextResponse.json({ demo: true, states: MOCK_MISSING });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { domain, service, entity_id, ...serviceData } = await req.json();
|
||||
const body = await req.json();
|
||||
const { domain, service, entity_id, ...serviceData } = body;
|
||||
|
||||
if (!HA_TOKEN) {
|
||||
return NextResponse.json({ success: true, demo: true });
|
||||
}
|
||||
|
||||
// Resolve alias: if normalized key, find real entity
|
||||
// Resolve alias
|
||||
const realEntityId = Object.keys(REAL_ENTITY_ALIASES).find(
|
||||
k => REAL_ENTITY_ALIASES[k] === entity_id
|
||||
) || entity_id;
|
||||
@@ -122,9 +144,12 @@ export async function POST(req: NextRequest) {
|
||||
body: JSON.stringify({ entity_id: realEntityId, ...serviceData }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HA responded ${res.status}`);
|
||||
if (!res.ok) {
|
||||
// Entity might not exist (mock) — return success anyway for local state
|
||||
return NextResponse.json({ success: true, mock: true });
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ success: false, error: String(e) }, { status: 500 });
|
||||
return NextResponse.json({ success: true, mock: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,3 +235,16 @@ input[type='range']::-moz-range-thumb {
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Safe area support for tablets/mobile */
|
||||
@supports (padding: max(0px)) {
|
||||
body {
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure 100dvh works properly */
|
||||
.h-dvh {
|
||||
height: 100dvh;
|
||||
height: 100svh;
|
||||
}
|
||||
|
||||
19
app/page.tsx
19
app/page.tsx
@@ -7,10 +7,10 @@ import BottomNav from "@/components/BottomNav";
|
||||
import LightCard from "@/components/cards/LightCard";
|
||||
import TemperatureCard from "@/components/cards/TemperatureCard";
|
||||
import AirPurifierCard from "@/components/cards/AirPurifierCard";
|
||||
import TasksCard from "@/components/cards/TasksCard";
|
||||
|
||||
import WeatherCard from "@/components/cards/WeatherCard";
|
||||
import RoomsRow from "@/components/RoomsRow";
|
||||
import { useHA, useWeather, useTasks } from "@/hooks/useHA";
|
||||
import { useHA, useWeather } from "@/hooks/useHA";
|
||||
|
||||
// Stagger container variants
|
||||
const containerVariants = {
|
||||
@@ -30,7 +30,6 @@ export default function Home() {
|
||||
|
||||
const { data: haData, loading: haLoading, refresh: refreshHA } = useHA(15000);
|
||||
const weather = useWeather();
|
||||
const { tasks, refresh: refreshTasks } = useTasks();
|
||||
|
||||
// Apply theme to html element
|
||||
useEffect(() => {
|
||||
@@ -299,20 +298,6 @@ export default function Home() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* ═══════════════ TASKS TAB ═══════════════ */}
|
||||
{activeTab === "tasks" && (
|
||||
<motion.div
|
||||
key="tasks"
|
||||
className="h-full max-w-2xl mx-auto w-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<TasksCard tasks={tasks} onUpdate={refreshTasks} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* ═══════════════ SETTINGS TAB ═══════════════ */}
|
||||
{activeTab === "settings" && (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user