313 lines
8.7 KiB
TypeScript
313 lines
8.7 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useCallback } from 'react'
|
||
import Sidebar from '@/components/Sidebar'
|
||
import TopBar from '@/components/TopBar'
|
||
import RoomTabs from '@/components/RoomTabs'
|
||
import DeviceCard from '@/components/DeviceCard'
|
||
|
||
type Tab = 'home' | 'rooms' | 'sensors' | 'settings'
|
||
|
||
interface WeatherData {
|
||
temp: string
|
||
desc: string
|
||
humidity: string
|
||
windSpeed: string
|
||
feelsLike: string
|
||
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string }[]
|
||
}
|
||
|
||
interface SensorData {
|
||
temperature: number
|
||
humidity: number
|
||
pm25: number
|
||
}
|
||
|
||
interface HaStates {
|
||
[key: string]: { state: string; attributes?: Record<string, any>; _mock?: boolean }
|
||
}
|
||
|
||
const ROOMS = [
|
||
{ id: 'living', name: 'Гостиная', emoji: '🛋️', deviceCount: 3 },
|
||
{ id: 'bedroom', name: 'Спальня', emoji: '🛏️', deviceCount: 2 },
|
||
{ id: 'kitchen', name: 'Кухня', emoji: '🍳', deviceCount: 0 },
|
||
{ id: 'bathroom', name: 'Ванная', emoji: '🚿', deviceCount: 0 },
|
||
]
|
||
|
||
const DEVICES_BY_ROOM: Record<string, {
|
||
id: string
|
||
name: string
|
||
icon: string
|
||
entityId?: string
|
||
domain?: string
|
||
haKey?: string
|
||
isMock?: boolean
|
||
}[]> = {
|
||
living: [
|
||
{
|
||
id: 'air_purifier',
|
||
name: 'Очиститель воздуха',
|
||
icon: '💨',
|
||
entityId: 'fan.zhimi_rmb1_9528_air_purifier',
|
||
domain: 'fan',
|
||
haKey: 'fan.air_purifier',
|
||
isMock: false,
|
||
},
|
||
{
|
||
id: 'light_living',
|
||
name: 'Свет',
|
||
icon: '💡',
|
||
entityId: 'light.living_room',
|
||
domain: 'light',
|
||
haKey: 'light.living_room',
|
||
isMock: true,
|
||
},
|
||
{
|
||
id: 'tv',
|
||
name: 'Телевизор',
|
||
icon: '📺',
|
||
isMock: true,
|
||
},
|
||
],
|
||
bedroom: [
|
||
{
|
||
id: 'light_bedroom',
|
||
name: 'Свет',
|
||
icon: '💡',
|
||
entityId: 'light.bedroom',
|
||
domain: 'light',
|
||
haKey: 'light.bedroom',
|
||
isMock: true,
|
||
},
|
||
{
|
||
id: 'ac',
|
||
name: 'Кондиционер',
|
||
icon: '❄️',
|
||
isMock: true,
|
||
},
|
||
],
|
||
kitchen: [],
|
||
bathroom: [],
|
||
}
|
||
|
||
export default function HomePage() {
|
||
const [tab, setTab] = useState<Tab>('home')
|
||
const [activeRoom, setActiveRoom] = useState('living')
|
||
const [weather, setWeather] = useState<WeatherData | null>(null)
|
||
const [sensors, setSensors] = useState<SensorData | null>(null)
|
||
const [haStates, setHaStates] = useState<HaStates>({})
|
||
|
||
// Load weather
|
||
useEffect(() => {
|
||
const load = async () => {
|
||
try {
|
||
const r = await fetch('/api/weather')
|
||
const d = await r.json()
|
||
if (d.temp && d.temp !== '—') setWeather(d)
|
||
} catch {}
|
||
}
|
||
load()
|
||
const t = setInterval(load, 600_000)
|
||
return () => clearInterval(t)
|
||
}, [])
|
||
|
||
// Load HA states + sensors
|
||
const loadHA = useCallback(async () => {
|
||
try {
|
||
const r = await fetch('/api/ha')
|
||
const d = await r.json()
|
||
if (d.states) setHaStates(d.states)
|
||
if (d.sensors) setSensors(d.sensors)
|
||
} catch {}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
loadHA()
|
||
const t = setInterval(loadHA, 30_000)
|
||
return () => clearInterval(t)
|
||
}, [loadHA])
|
||
|
||
const devicesInRoom = DEVICES_BY_ROOM[activeRoom] || []
|
||
|
||
const getDeviceState = (haKey?: string): boolean => {
|
||
if (!haKey || !haStates[haKey]) return false
|
||
return haStates[haKey].state === 'on'
|
||
}
|
||
|
||
const getDeviceExtra = (id: string): string | undefined => {
|
||
if (id === 'air_purifier' && sensors) {
|
||
return `PM2.5: ${sensors.pm25}`
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
height: '100dvh',
|
||
width: '100%',
|
||
background: 'var(--bg)',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
<Sidebar active={tab} onChange={setTab} />
|
||
|
||
<main
|
||
style={{
|
||
flex: 1,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
overflow: 'hidden',
|
||
minWidth: 0,
|
||
}}
|
||
>
|
||
<TopBar weather={weather} sensors={sensors} />
|
||
|
||
{tab === 'home' && (
|
||
<>
|
||
<RoomTabs rooms={ROOMS} active={activeRoom} onChange={setActiveRoom} />
|
||
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
overflowY: 'auto',
|
||
WebkitOverflowScrolling: 'touch' as any,
|
||
padding: '16px 20px 24px',
|
||
}}
|
||
>
|
||
{devicesInRoom.length === 0 ? (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
height: 200,
|
||
color: 'var(--text-secondary)',
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 40 }}>🏠</span>
|
||
<span style={{ fontSize: 15 }}>Устройства не добавлены</span>
|
||
</div>
|
||
) : (
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
{devicesInRoom.map(device => (
|
||
<DeviceCard
|
||
key={device.id}
|
||
id={device.id}
|
||
name={device.name}
|
||
icon={device.icon}
|
||
entityId={device.entityId}
|
||
domain={device.domain}
|
||
initialState={getDeviceState(device.haKey)}
|
||
isMock={device.isMock}
|
||
extraInfo={getDeviceExtra(device.id)}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{tab === 'rooms' && (
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 12,
|
||
color: 'var(--text-secondary)',
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 48 }}>🏠</span>
|
||
<span style={{ fontSize: 16 }}>Управление комнатами</span>
|
||
<span style={{ fontSize: 13 }}>Скоро</span>
|
||
</div>
|
||
)}
|
||
|
||
{tab === 'sensors' && sensors && (
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
overflowY: 'auto',
|
||
padding: '20px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<h2 style={{ fontSize: 18, fontWeight: 600, marginBottom: 8 }}>Датчики</h2>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
|
||
{[
|
||
{ label: 'Температура', value: `${sensors.temperature}°C`, icon: '🌡️' },
|
||
{ label: 'Влажность', value: `${sensors.humidity}%`, icon: '💧' },
|
||
{ label: 'PM2.5', value: `${sensors.pm25} μg/m³`, icon: '💨' },
|
||
].map(s => (
|
||
<div
|
||
key={s.label}
|
||
style={{
|
||
background: 'var(--card-bg)',
|
||
border: '1px solid var(--card-border)',
|
||
borderRadius: 18,
|
||
padding: 20,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 28 }}>{s.icon}</span>
|
||
<div style={{ fontSize: 22, fontWeight: 700 }}>{s.value}</div>
|
||
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{s.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{tab === 'sensors' && !sensors && (
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
color: 'var(--text-secondary)',
|
||
}}
|
||
>
|
||
Загрузка датчиков...
|
||
</div>
|
||
)}
|
||
|
||
{tab === 'settings' && (
|
||
<div
|
||
style={{
|
||
flex: 1,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 12,
|
||
color: 'var(--text-secondary)',
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 48 }}>⚙️</span>
|
||
<span style={{ fontSize: 16 }}>Настройки</span>
|
||
<span style={{ fontSize: 13 }}>Скоро</span>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|