WEB UI
This commit is contained in:
@@ -84,11 +84,23 @@ async def blink_device(device_id: str):
|
|||||||
if not device:
|
if not device:
|
||||||
raise HTTPException(status_code=404, detail="Лампа оффлайн")
|
raise HTTPException(status_code=404, detail="Лампа оффлайн")
|
||||||
|
|
||||||
await wiz.set_pilot(device.ip, {"sceneId": 34, "speed": 100})
|
try:
|
||||||
await asyncio.sleep(3)
|
# 1. Получаем текущее состояние
|
||||||
await wiz.set_pilot(device.ip, {"state": False})
|
current = await wiz.get_pilot(device.ip)
|
||||||
|
# Если не удалось получить статус, считаем что она выключена (False)
|
||||||
|
original_state = current.get("result", {}).get("state", False)
|
||||||
|
|
||||||
return {"status": "blink_sent"}
|
# 2. Инвертируем состояние
|
||||||
|
await wiz.set_pilot(device.ip, {"state": not original_state})
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# 3. Возвращаем как было
|
||||||
|
await wiz.set_pilot(device.ip, {"state": original_state})
|
||||||
|
|
||||||
|
return {"status": "blink_done", "original": original_state}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Blink error: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Ошибка связи с лампой")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/device/{device_id}/status")
|
@router.get("/device/{device_id}/status")
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -50,7 +50,15 @@ app.include_router(control.router, prefix="/control", tags=["Control"])
|
|||||||
app.include_router(schedules.router, prefix="/schedules", tags=["Schedules"])
|
app.include_router(schedules.router, prefix="/schedules", tags=["Schedules"])
|
||||||
|
|
||||||
# Статика
|
# Статика
|
||||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
# Мы убираем html=True из корня, чтобы 404-е ошибки API не превращались в загрузку index.html
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def read_index():
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
return FileResponse("static/index.html")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -3,133 +3,141 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Ignis Control Center</title>
|
<title>IGNIS | Smart Control</title>
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body { background: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; }
|
body {
|
||||||
.card { background: #1e293b; border: 1px solid #334155; }
|
background: radial-gradient(circle at top right, #1e1b4b, #0f172a);
|
||||||
.btn-primary { @apply bg-orange-600 hover:bg-orange-500 text-white px-4 py-2 rounded-xl font-bold transition-all disabled:opacity-50; }
|
color: #f8fafc;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
/* ЖЕСТКО ЗАДАННЫЕ СТИЛИ ДЛЯ ИНПУТОВ */
|
min-height: 100vh;
|
||||||
.input-dark {
|
|
||||||
background-color: #0f172a !important;
|
|
||||||
color: #ffffff !important;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
outline: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.input-dark:focus { border-color: #f97316; }
|
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.1); }
|
||||||
.input-dark::placeholder { color: #64748b; }
|
.active-tab { background: #f97316; box-shadow: 0 0 20px rgba(249, 115, 22, 0.4); }
|
||||||
|
input[type="range"] { -webkit-appearance: none; height: 6px; border-radius: 10px; background: #334155; outline: none; }
|
||||||
.temp-gradient { background: linear-gradient(to right, #ffb366, #ffffff, #99ccff); }
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
input[type="range"] { -webkit-appearance: none; height: 8px; border-radius: 4px; background: #334155; }
|
-webkit-appearance: none; width: 18px; height: 18px;
|
||||||
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: white; border-radius: 50%; border: 2px solid #f97316; cursor: pointer; }
|
background: #fff; border-radius: 50%; cursor: pointer; border: 3px solid #f97316;
|
||||||
|
}
|
||||||
|
.temp-gradient { background: linear-gradient(to right, #ffcc66, #ffffff, #99ccff) !important; }
|
||||||
|
select { appearance: none; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 1rem center; background-size: 1em; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="p-4 md:p-8 max-w-5xl mx-auto">
|
<div id="app" class="max-w-6xl mx-auto p-4 md:p-10">
|
||||||
<header class="mb-10 flex justify-between items-center">
|
|
||||||
<div class="flex items-center gap-3">
|
<div v-if="!apiKey" class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/90 backdrop-blur-sm p-4">
|
||||||
<span class="text-4xl">🔥</span>
|
<div class="glass p-8 rounded-[2.5rem] w-full max-w-md shadow-2xl">
|
||||||
<h1 class="text-3xl font-black text-orange-500 tracking-tighter uppercase">Ignis</h1>
|
<div class="text-center mb-8">
|
||||||
|
<span class="text-5xl mb-4 block">🔐</span>
|
||||||
|
<h2 class="text-2xl font-black uppercase tracking-tighter">Ignis Access</h2>
|
||||||
|
<p class="text-slate-400 text-sm mt-2">Введите API ключ из .env</p>
|
||||||
|
</div>
|
||||||
|
<input v-model="tempKey" type="password" placeholder="X-API-Key"
|
||||||
|
@keyup.enter="saveKey"
|
||||||
|
class="w-full bg-slate-900 border border-slate-700 p-4 rounded-2xl mb-4 focus:border-orange-500 outline-none text-center tracking-widest">
|
||||||
|
<button @click="saveKey" class="w-full bg-orange-600 hover:bg-orange-500 py-4 rounded-2xl font-bold transition-all shadow-lg shadow-orange-900/40">
|
||||||
|
ВОЙТИ
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 bg-slate-800/50 p-1 rounded-2xl border border-slate-700">
|
</div>
|
||||||
<button @click="tab = 'control'" :class="tab === 'control' ? 'bg-slate-700 text-white shadow-lg' : 'text-slate-400'" class="px-6 py-2 rounded-xl font-bold transition-all">Пульт</button>
|
|
||||||
<button @click="tab = 'admin'" :class="tab === 'admin' ? 'bg-slate-700 text-white shadow-lg' : 'text-slate-400'" class="px-6 py-2 rounded-xl font-bold transition-all">Настройки</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main v-if="tab === 'control'" class="space-y-6">
|
<template v-else>
|
||||||
<div v-if="Object.keys(groups).length === 0" class="text-center py-20 opacity-50 uppercase tracking-widest">Групп пока нет. Зайди в настройки.</div>
|
<header class="flex flex-col md:flex-row justify-between items-center mb-12 gap-6">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="flex items-center gap-4">
|
||||||
<div v-for="(group, id) in groups" :key="id" class="card p-6 rounded-3xl shadow-2xl">
|
<div class="bg-orange-600 p-3 rounded-2xl shadow-lg">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
<h2 class="text-2xl font-bold text-slate-100">{{ group.name }}</h2>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl font-extrabold tracking-tighter uppercase italic">Ignis<span class="text-orange-500">Core</span></h1>
|
||||||
|
<button @click="logout" class="text-[10px] text-slate-500 hover:text-red-400 uppercase font-bold tracking-widest transition-colors">Выйти</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex glass p-1.5 rounded-2xl">
|
||||||
|
<button @click="tab = 'control'" :class="tab === 'control' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">ПУЛЬТ</button>
|
||||||
|
<button @click="tab = 'admin'" :class="tab === 'admin' ? 'active-tab' : 'text-slate-400'" class="px-8 py-2.5 rounded-xl font-bold transition-all">АДМИНКА</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="tab === 'control'" class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<div v-if="Object.keys(groups).length === 0" class="col-span-full text-center py-20 glass rounded-3xl opacity-50">
|
||||||
|
<p class="text-xl italic text-slate-400">Создайте группу в админке, чтобы управлять лампами</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(group, id) in groups" :key="id" class="glass p-8 rounded-[2.5rem] relative group hover:border-orange-500/50 transition-all">
|
||||||
|
<div class="flex justify-between items-start mb-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-black text-white">{{ group.name }}</h2>
|
||||||
|
<span class="text-[10px] font-mono text-slate-500 uppercase tracking-tighter">{{ id }} • {{ group.device_ids?.length || 0 }} ламп</span>
|
||||||
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="toggleGroup(id, true)" class="bg-orange-600 hover:bg-orange-500 px-4 py-1 rounded-lg text-sm font-bold transition-all">ВКЛ</button>
|
<button @click="deleteGroup(id)" class="bg-slate-800 hover:bg-red-900/40 p-3 rounded-xl transition-all text-slate-500 hover:text-red-500" title="Удалить группу">
|
||||||
<button @click="toggleGroup(id, false)" class="bg-slate-700 hover:bg-slate-600 px-4 py-1 rounded-lg text-sm font-bold transition-all">ВЫКЛ</button>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="toggleGroup(id, true)" class="bg-orange-600 hover:bg-orange-500 p-3 rounded-xl font-bold px-5 transition-all">ВКЛ</button>
|
||||||
|
<button @click="toggleGroup(id, false)" class="bg-slate-700 hover:bg-slate-600 p-3 rounded-xl font-bold px-5 transition-all">ВЫКЛ</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between text-[10px] font-black uppercase text-slate-500 mb-2"><span>Яркость</span><span class="text-orange-400">{{ group.brightness || 100 }}%</span></div>
|
<div class="flex justify-between text-[10px] font-black uppercase text-slate-400 mb-3"><span>Яркость</span><span class="text-orange-400">{{ sliders[id]?.brightness }}%</span></div>
|
||||||
<input type="range" min="10" max="100" class="w-full" v-model="group.brightness" @change="setBrightness(id, $event.target.value)">
|
<input type="range" min="10" max="100" class="w-full" v-model="sliders[id].brightness" @change="setBrightness(id, $event.target.value)">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between text-[10px] font-black uppercase text-slate-500 mb-2"><span>Температура</span><span class="text-blue-400">{{ group.temp || 3000 }}K</span></div>
|
<div class="flex justify-between text-[10px] font-black uppercase text-slate-400 mb-3"><span>Температура</span><span class="text-blue-300">{{ sliders[id]?.temp }}K</span></div>
|
||||||
<input type="range" min="2700" max="6500" step="100" class="w-full temp-gradient" v-model="group.temp" @change="setTemp(id, $event.target.value)">
|
<input type="range" min="2700" max="6500" step="100" class="w-full temp-gradient" v-model="sliders[id].temp" @change="setTemp(id, $event.target.value)">
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-black uppercase text-slate-500 mb-2 block">Цвет</label>
|
<label class="text-[10px] font-black uppercase text-slate-400 mb-3 block">Цвет</label>
|
||||||
<input type="color" class="w-full h-10 bg-transparent border border-slate-700 rounded-lg cursor-pointer" @input="setColor(id, $event.target.value)">
|
<input type="color" class="w-full h-12 bg-transparent border-2 border-slate-700 rounded-xl cursor-pointer" @input="setColor(id, $event.target.value)">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-black uppercase text-slate-500 mb-2 block">Сцены</label>
|
<label class="text-[10px] font-black uppercase text-slate-400 mb-3 block">Все сцены</label>
|
||||||
<div class="flex flex-wrap gap-1">
|
<select @change="setScene(id, $event.target.value)" class="w-full bg-slate-900 border border-slate-700 p-3 rounded-xl text-sm outline-none focus:border-orange-500">
|
||||||
<button v-for="s in ['ocean', 'party', 'steampunk', 'fireplace']" @click="setScene(id, s)" class="text-[8px] font-bold border border-slate-700 px-2 py-1 rounded hover:bg-slate-800 uppercase transition-all">{{s}}</button>
|
<option value="" disabled selected>Выбрать пресет...</option>
|
||||||
</div>
|
<option v-for="scene in allScenes" :key="scene" :value="scene">{{ scene }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
|
||||||
<main v-if="tab === 'admin'" class="space-y-10">
|
<div v-if="tab === 'admin'" class="space-y-10">
|
||||||
<section class="card p-6 rounded-3xl">
|
<section class="glass p-8 rounded-[2.5rem]">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<h2 class="text-xl font-bold italic text-orange-400">Новая группа</h2>
|
<h2 class="text-2xl font-bold italic">Устройства в сети</h2>
|
||||||
<button @click="rescan" class="text-[10px] font-black bg-blue-600/10 text-blue-400 border border-blue-500/20 px-4 py-2 rounded-xl hover:bg-blue-600/20 transition-all">
|
<button @click="rescan" class="bg-blue-600 hover:bg-blue-500 px-6 py-2 rounded-xl text-xs font-black transition-all">🔄 ПЕРЕСКАНИРОВАТЬ</button>
|
||||||
🔄 ПЕРЕСКАН СЕТИ
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row gap-4 mb-8">
|
|
||||||
<input v-model="newGroup.id" placeholder="ID (например: kitchen)" class="input-dark">
|
|
||||||
<input v-model="newGroup.name" placeholder="Имя (например: Кухня)" class="input-dark">
|
|
||||||
<button @click="createGroup" :disabled="!newGroup.id || !newGroup.macs.length" class="btn-primary min-w-[140px]">СОЗДАТЬ</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-10">
|
||||||
<p class="text-xs font-black text-slate-500 mb-4 uppercase tracking-widest">Доступные лампы в сети:</p>
|
<input v-model="newGroup.id" placeholder="ID (например, bedroom)" class="bg-slate-900 border border-slate-700 p-4 rounded-xl focus:border-orange-500 outline-none">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
<input v-model="newGroup.name" placeholder="Имя (например, Спальня)" class="bg-slate-900 border border-slate-700 p-4 rounded-xl focus:border-orange-500 outline-none">
|
||||||
|
<button @click="createGroup" :disabled="!newGroup.id || !newGroup.macs.length"
|
||||||
|
class="bg-orange-600 hover:bg-orange-500 disabled:opacity-30 rounded-xl font-black transition-all shadow-lg">СОЗДАТЬ ГРУППУ</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<div v-for="dev in devices" :key="dev.id"
|
<div v-for="dev in devices" :key="dev.id"
|
||||||
class="flex items-center justify-between p-4 bg-slate-900/50 rounded-2xl border border-slate-700 hover:border-slate-500 transition-all">
|
class="bg-slate-900/50 border border-slate-800 p-4 rounded-2xl flex items-center justify-between hover:border-slate-600 transition-all">
|
||||||
<label class="flex items-center gap-3 cursor-pointer flex-1">
|
<label class="flex items-center gap-4 cursor-pointer flex-1">
|
||||||
<input type="checkbox" :value="dev.id" v-model="newGroup.macs" class="w-5 h-5 accent-orange-500">
|
<input type="checkbox" :value="dev.id" v-model="newGroup.macs" class="w-5 h-5 accent-orange-500">
|
||||||
<div class="text-xs">
|
<div>
|
||||||
<div class="font-bold text-slate-200">{{ dev.id }}</div>
|
<p class="font-bold text-sm">{{ dev.id }}</p>
|
||||||
<div class="text-slate-500 font-mono text-[10px]">{{ dev.ip }}</div>
|
<p class="text-[10px] font-mono text-slate-500">{{ dev.ip }}</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<button @click="blink(dev.id)" class="p-2 hover:bg-orange-500/20 rounded-xl text-orange-400 transition-all">
|
<button @click="blink(dev.id)" class="p-3 bg-slate-800 rounded-xl hover:bg-orange-600 transition-all shadow-md" title="Мигнуть лампой">👁️</button>
|
||||||
👁️
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
|
</template>
|
||||||
<section class="card p-6 rounded-3xl">
|
|
||||||
<h2 class="text-xl font-bold mb-6 text-slate-400 uppercase tracking-tighter">Управление группами в базе</h2>
|
|
||||||
<div class="grid grid-cols-1 gap-3">
|
|
||||||
<div v-for="(group, id) in groups" :key="id" class="flex justify-between items-center p-5 bg-slate-900/30 rounded-2xl border border-slate-800">
|
|
||||||
<div>
|
|
||||||
<div class="font-bold text-slate-100 text-lg">{{ group.name }}</div>
|
|
||||||
<div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">{{ id }} • {{ group.device_ids.length }} ламп</div>
|
|
||||||
</div>
|
|
||||||
<button @click="deleteGroup(id)" class="text-red-500 text-[10px] font-black border border-red-500/20 px-4 py-2 rounded-xl hover:bg-red-500 hover:text-white transition-all">
|
|
||||||
УДАЛИТЬ
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -137,61 +145,102 @@
|
|||||||
createApp({
|
createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
apiKey: localStorage.getItem('ignis_key') || '',
|
||||||
|
tempKey: '',
|
||||||
tab: 'control',
|
tab: 'control',
|
||||||
groups: {},
|
groups: {},
|
||||||
devices: {},
|
devices: [],
|
||||||
newGroup: { id: '', name: '', macs: [] }
|
sliders: {},
|
||||||
|
newGroup: { id: '', name: '', macs: [] },
|
||||||
|
allScenes: [
|
||||||
|
"ocean", "romance", "party", "fireplace", "cozy", "forest",
|
||||||
|
"pastel_colors", "wake_up", "bedtime", "warm_white", "daylight",
|
||||||
|
"cool_white", "night_light", "focus", "relax", "true_colors",
|
||||||
|
"tv_time", "plant_growth", "spring", "summer", "fall", "deep_dive",
|
||||||
|
"jungle", "mojito", "club", "christmas", "halloween", "candlelight",
|
||||||
|
"golden_white", "pulse", "steampunk"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchData() {
|
saveKey() {
|
||||||
try {
|
if (this.tempKey) {
|
||||||
const gResp = await fetch('/groups');
|
this.apiKey = this.tempKey;
|
||||||
this.groups = await gResp.json();
|
localStorage.setItem('ignis_key', this.tempKey);
|
||||||
const dResp = await fetch('/devices');
|
this.fetchData();
|
||||||
this.devices = await dResp.json();
|
|
||||||
} catch (e) { console.error("Ошибка сети", e); }
|
|
||||||
},
|
|
||||||
async control(id, params) {
|
|
||||||
const query = new URLSearchParams(params).toString();
|
|
||||||
await fetch(`/control/group/${id}?${query}`, { method: 'POST' });
|
|
||||||
},
|
|
||||||
async createGroup() {
|
|
||||||
const resp = await fetch('/groups', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({ id: this.newGroup.id, name: this.newGroup.name, macs: this.newGroup.macs })
|
|
||||||
});
|
|
||||||
if(resp.ok) {
|
|
||||||
this.newGroup = { id: '', name: '', macs: [] };
|
|
||||||
await this.fetchData();
|
|
||||||
this.tab = 'control';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async deleteGroup(id) {
|
logout() {
|
||||||
if(!confirm(`Удалить группу "${id}" из базы навсегда?`)) return;
|
this.apiKey = '';
|
||||||
const resp = await fetch(`/groups/${id}`, { method: 'DELETE' });
|
localStorage.removeItem('ignis_key');
|
||||||
if(resp.ok) await this.fetchData();
|
location.reload();
|
||||||
},
|
},
|
||||||
async rescan() {
|
async request(path, method = 'GET', params = null, body = null) {
|
||||||
await fetch('/discovery/rescan', { method: 'POST' });
|
let url = path;
|
||||||
await this.fetchData();
|
if (params) {
|
||||||
|
const q = new URLSearchParams(params).toString();
|
||||||
|
url += `?${q}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': this.apiKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : null
|
||||||
|
});
|
||||||
|
if (response.status === 403) { this.logout(); return null; }
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return await response.json();
|
||||||
|
} catch (e) { return null; }
|
||||||
},
|
},
|
||||||
async blink(deviceId) {
|
async fetchData() {
|
||||||
await fetch(`/control/device/${deviceId}/blink`, { method: 'POST' });
|
if (!this.apiKey) return;
|
||||||
|
const gData = await this.request('/devices/groups');
|
||||||
|
const dData = await this.request('/devices');
|
||||||
|
if (gData) this.groups = gData;
|
||||||
|
if (dData) this.devices = Object.values(dData);
|
||||||
|
Object.keys(this.groups).forEach(id => {
|
||||||
|
if (!this.sliders[id]) this.sliders[id] = { brightness: 100, temp: 3000 };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
toggleGroup(id, state) { this.control(id, { state }); },
|
async control(id, params) { await this.request(`/control/group/${id}`, 'POST', params); },
|
||||||
|
toggleGroup(id, state) { this.control(id, { state: state }); },
|
||||||
setBrightness(id, val) { this.control(id, { brightness: val }); },
|
setBrightness(id, val) { this.control(id, { brightness: val }); },
|
||||||
setTemp(id, val) { this.control(id, { temp: val }); },
|
setTemp(id, val) { this.control(id, { temp: val }); },
|
||||||
setScene(id, scene) { this.control(id, { scene }); },
|
setScene(id, scene) { this.control(id, { scene }); },
|
||||||
setColor(id, hex) {
|
setColor(id, hex) {
|
||||||
const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
|
const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
|
||||||
this.control(id, { r, g, b });
|
this.control(id, { r, g, b });
|
||||||
}
|
},
|
||||||
|
async createGroup() {
|
||||||
|
const res = await this.request('/devices/groups', 'POST', null, {
|
||||||
|
id: this.newGroup.id, name: this.newGroup.name, macs: this.newGroup.macs
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
this.newGroup = { id: '', name: '', macs: [] };
|
||||||
|
await this.fetchData();
|
||||||
|
this.tab = 'control';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteGroup(id) {
|
||||||
|
if (confirm(`Удалить группу ${id}?`)) {
|
||||||
|
await this.request(`/devices/groups/${id}`, 'DELETE');
|
||||||
|
await this.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async rescan() {
|
||||||
|
await this.request('/devices/rescan', 'POST');
|
||||||
|
setTimeout(this.fetchData, 2000);
|
||||||
|
},
|
||||||
|
async blink(deviceId) { await this.request(`/control/device/${deviceId}/blink`, 'POST'); }
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchData();
|
if (this.apiKey) {
|
||||||
setInterval(this.fetchData, 5000);
|
this.fetchData();
|
||||||
|
setInterval(this.fetchData, 15000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).mount('#app')
|
}).mount('#app')
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user