366 lines
12 KiB
Dart
366 lines
12 KiB
Dart
import 'dart:async';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../models/home_config.dart';
|
||
import '../services/api_client.dart';
|
||
import '../services/settings_service.dart';
|
||
|
||
// ─── Сервисы ─────────────────────────────────────────────────
|
||
|
||
/// Синглтон сервиса настроек
|
||
final settingsServiceProvider = Provider((ref) => SettingsService());
|
||
|
||
/// API-клиент -- пересоздаётся при смене дома
|
||
final apiProvider = Provider((ref) => IgnisApi());
|
||
|
||
// ─── Текущий дом ─────────────────────────────────────────────
|
||
|
||
/// Текущий выбранный дом (null если ни одного нет)
|
||
final currentHomeProvider =
|
||
NotifierProvider<CurrentHomeNotifier, HomeConfig?>(() => CurrentHomeNotifier());
|
||
|
||
class CurrentHomeNotifier extends Notifier<HomeConfig?> {
|
||
@override
|
||
HomeConfig? build() => null;
|
||
|
||
/// Загрузить текущий дом из SharedPreferences
|
||
Future<void> load() async {
|
||
final svc = ref.read(settingsServiceProvider);
|
||
state = await svc.getCurrentHome();
|
||
if (state != null) {
|
||
_initApi(state!);
|
||
}
|
||
}
|
||
|
||
/// Переключиться на другой дом
|
||
Future<void> switchTo(HomeConfig home) async {
|
||
final svc = ref.read(settingsServiceProvider);
|
||
await svc.setCurrentHomeId(home.id);
|
||
state = home;
|
||
_initApi(home);
|
||
// Перезагрузить группы для нового дома
|
||
await ref.read(groupsProvider.notifier).initAndRefresh();
|
||
}
|
||
|
||
/// Инициализировать API-клиент текущим домом
|
||
void _initApi(HomeConfig home) {
|
||
ref.read(apiProvider).init(home.url, home.apiKey);
|
||
}
|
||
}
|
||
|
||
// ─── Список домов ────────────────────────────────────────────
|
||
|
||
final homesProvider =
|
||
NotifierProvider<HomesNotifier, List<HomeConfig>>(() => HomesNotifier());
|
||
|
||
class HomesNotifier extends Notifier<List<HomeConfig>> {
|
||
@override
|
||
List<HomeConfig> build() => [];
|
||
|
||
Future<void> load() async {
|
||
state = await ref.read(settingsServiceProvider).getHomes();
|
||
}
|
||
|
||
Future<void> add(HomeConfig home) async {
|
||
await ref.read(settingsServiceProvider).upsertHome(home);
|
||
await load();
|
||
}
|
||
|
||
Future<void> remove(String id) async {
|
||
await ref.read(settingsServiceProvider).deleteHome(id);
|
||
await load();
|
||
}
|
||
|
||
Future<void> update(HomeConfig home) async {
|
||
await ref.read(settingsServiceProvider).upsertHome(home);
|
||
await load();
|
||
}
|
||
}
|
||
|
||
// ─── Группы текущего дома ────────────────────────────────────
|
||
|
||
final groupsProvider =
|
||
NotifierProvider<GroupsNotifier, List<dynamic>>(() => GroupsNotifier());
|
||
|
||
class GroupsNotifier extends Notifier<List<dynamic>> {
|
||
IgnisApi get _api => ref.read(apiProvider);
|
||
Timer? _timer;
|
||
|
||
/// Блокировка обновления для группы после управления --
|
||
/// чтобы UI не прыгал пока лампа ещё не ответила.
|
||
final Map<String, DateTime> _lockUntil = {};
|
||
|
||
@override
|
||
List<dynamic> build() {
|
||
ref.onDispose(() => _timer?.cancel());
|
||
return [];
|
||
}
|
||
|
||
/// Инициализация: настроить API и начать периодический опрос
|
||
Future<void> initAndRefresh() async {
|
||
final home = ref.read(currentHomeProvider);
|
||
if (home == null) return;
|
||
_api.init(home.url, home.apiKey);
|
||
await refresh();
|
||
_timer?.cancel();
|
||
_timer = Timer.periodic(const Duration(seconds: 10), (_) => refresh());
|
||
}
|
||
|
||
/// Полный опрос: загрузить группы + статус каждой
|
||
Future<void> refresh() async {
|
||
try {
|
||
final resGroups = await _api.getGroups();
|
||
List<dynamic> rawList = [];
|
||
|
||
// Бэкенд может вернуть и Map, и List -- обрабатываем оба варианта
|
||
if (resGroups.data is Map) {
|
||
rawList = resGroups.data['data'] ??
|
||
resGroups.data['groups'] ??
|
||
resGroups.data.values.toList();
|
||
} else if (resGroups.data is List) {
|
||
rawList = resGroups.data;
|
||
}
|
||
|
||
final now = DateTime.now();
|
||
|
||
// Параллельный опрос статусов всех групп
|
||
final updatedList = await Future.wait(rawList.map((g) async {
|
||
final map = Map<String, dynamic>.from(g);
|
||
final id = map['id'].toString();
|
||
|
||
// Если группа залочена (недавно управляли) -- берём локальное состояние
|
||
if (_lockUntil.containsKey(id) && _lockUntil[id]!.isAfter(now)) {
|
||
final existing = state.cast<dynamic?>().firstWhere(
|
||
(old) => old?['id'].toString() == id,
|
||
orElse: () => null);
|
||
return existing ?? map;
|
||
}
|
||
|
||
try {
|
||
final resStatus = await _api.getGroupStatus(id);
|
||
// Формат ответа: { results: [ { status: { state, dimming, temp, ... } } ] }
|
||
if (resStatus.data != null &&
|
||
resStatus.data['results'] is List &&
|
||
resStatus.data['results'].isNotEmpty) {
|
||
final data = resStatus.data['results'][0]['status'];
|
||
map['last_state'] = {
|
||
'state': data['state'] == true,
|
||
'brightness': (data['dimming'] ?? 100).toInt(),
|
||
'temp': (data['temp'] ?? 4000).toInt(),
|
||
'r': (data['r'] ?? 0).toInt(),
|
||
'g': (data['g'] ?? 0).toInt(),
|
||
'b': (data['b'] ?? 0).toInt(),
|
||
'scene': data['scene'],
|
||
};
|
||
}
|
||
} catch (e) {
|
||
// При ошибке опроса -- сохраняем предыдущее состояние
|
||
final existing = state.cast<dynamic?>().firstWhere(
|
||
(s) => s?['id'].toString() == id,
|
||
orElse: () => null);
|
||
map['last_state'] = existing?['last_state'] ??
|
||
{'state': false, 'brightness': 100, 'temp': 4000};
|
||
}
|
||
return map;
|
||
}));
|
||
|
||
state = updatedList;
|
||
} catch (e) {
|
||
debugPrint("Ошибка глобального опроса: $e");
|
||
}
|
||
}
|
||
|
||
/// Установить блокировку на 5 секунд (чтобы UI не перетирал значения)
|
||
void _setLock(String id) =>
|
||
_lockUntil[id] = DateTime.now().add(const Duration(seconds: 5));
|
||
|
||
/// Обновить локальное состояние группы (оптимистичный UI)
|
||
void _updateLocal(String id, Map<String, dynamic> patch) {
|
||
state = [
|
||
for (final g in state)
|
||
if (g['id'].toString() == id)
|
||
{
|
||
...g,
|
||
'last_state': {
|
||
...Map<String, dynamic>.from(g['last_state'] ?? {}),
|
||
...patch
|
||
}
|
||
}
|
||
else
|
||
g
|
||
];
|
||
}
|
||
|
||
/// Включить/выключить группу
|
||
Future<void> toggleGroup(String id, bool on) async {
|
||
_setLock(id);
|
||
_updateLocal(id, {'state': on});
|
||
try {
|
||
await _api.controlGroup(id, {'state': on});
|
||
} catch (e) {
|
||
_lockUntil.remove(id);
|
||
refresh();
|
||
}
|
||
}
|
||
|
||
/// Установить яркость (0-100)
|
||
Future<void> setBrightness(String id, int value) async {
|
||
_setLock(id);
|
||
_updateLocal(id, {'brightness': value});
|
||
await _api.controlGroup(id, {'brightness': value});
|
||
}
|
||
|
||
/// Установить цветовую температуру (2700-6500K)
|
||
Future<void> setTemperature(String id, int value) async {
|
||
_setLock(id);
|
||
_updateLocal(id, {'temp': value});
|
||
await _api.controlGroup(id, {'temp': value});
|
||
}
|
||
|
||
/// Установить RGB-цвет
|
||
Future<void> setColor(String id, int r, int g, int b) async {
|
||
_setLock(id);
|
||
_updateLocal(id, {'r': r, 'g': g, 'b': b});
|
||
await _api.controlGroup(id, {'r': r, 'g': g, 'b': b});
|
||
}
|
||
|
||
/// Установить сцену
|
||
Future<void> setScene(String id, String scene) async {
|
||
_setLock(id);
|
||
_updateLocal(id, {'scene': scene});
|
||
await _api.controlGroup(id, {'scene': scene});
|
||
}
|
||
|
||
/// Таймер: включить на 4 часа
|
||
Future<void> setTimer4h(String id) async {
|
||
await toggleGroup(id, true);
|
||
await _api.scheduleOnce({
|
||
'target_id': id,
|
||
'state': false,
|
||
'hours_from_now': 4,
|
||
'is_group': true,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── Устройства (для создания групп) ─────────────────────────
|
||
|
||
final devicesProvider =
|
||
NotifierProvider<DevicesNotifier, List<dynamic>>(() => DevicesNotifier());
|
||
|
||
class DevicesNotifier extends Notifier<List<dynamic>> {
|
||
@override
|
||
List<dynamic> build() => [];
|
||
|
||
/// Загрузить список устройств из текущего дома
|
||
Future<void> load() async {
|
||
try {
|
||
final api = ref.read(apiProvider);
|
||
final res = await api.getDevices();
|
||
if (res.data is List) {
|
||
state = res.data;
|
||
} else if (res.data is Map) {
|
||
state = res.data['data'] ?? res.data['devices'] ?? res.data.values.toList();
|
||
}
|
||
} catch (e) {
|
||
debugPrint("Ошибка загрузки устройств: $e");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Сцены ───────────────────────────────────────────────────
|
||
|
||
final scenesProvider =
|
||
NotifierProvider<ScenesNotifier, List<dynamic>>(() => ScenesNotifier());
|
||
|
||
class ScenesNotifier extends Notifier<List<dynamic>> {
|
||
@override
|
||
List<dynamic> build() => [];
|
||
|
||
Future<void> load() async {
|
||
try {
|
||
final api = ref.read(apiProvider);
|
||
final res = await api.getScenes();
|
||
if (res.data is List) {
|
||
state = res.data;
|
||
} else if (res.data is Map) {
|
||
state = res.data['data'] ?? res.data['scenes'] ?? res.data.values.toList();
|
||
}
|
||
} catch (e) {
|
||
debugPrint("Ошибка загрузки сцен: $e");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── Расписания ──────────────────────────────────────────────
|
||
|
||
final tasksProvider =
|
||
NotifierProvider<TasksNotifier, List<dynamic>>(() => TasksNotifier());
|
||
|
||
class TasksNotifier extends Notifier<List<dynamic>> {
|
||
@override
|
||
List<dynamic> build() => [];
|
||
|
||
Future<void> load() async {
|
||
try {
|
||
final api = ref.read(apiProvider);
|
||
final res = await api.getTasks();
|
||
if (res.data is List) {
|
||
state = res.data;
|
||
} else if (res.data is Map) {
|
||
state = res.data['data'] ?? res.data['tasks'] ?? res.data.values.toList();
|
||
}
|
||
} catch (e) {
|
||
debugPrint("Ошибка загрузки расписаний: $e");
|
||
}
|
||
}
|
||
|
||
Future<void> cancel(String jobId) async {
|
||
try {
|
||
await ref.read(apiProvider).cancelTask(jobId);
|
||
await load();
|
||
} catch (e) {
|
||
debugPrint("Ошибка отмены задачи: $e");
|
||
}
|
||
}
|
||
|
||
/// Создать одноразовый таймер
|
||
Future<void> addOnce({
|
||
required String targetId,
|
||
required bool targetState,
|
||
int? hoursFromNow,
|
||
String? runAt,
|
||
bool isGroup = true,
|
||
}) async {
|
||
final params = <String, dynamic>{
|
||
'target_id': targetId,
|
||
'state': targetState,
|
||
'is_group': isGroup,
|
||
};
|
||
if (hoursFromNow != null) params['hours_from_now'] = hoursFromNow;
|
||
if (runAt != null) params['run_at'] = runAt;
|
||
await ref.read(apiProvider).scheduleOnce(params);
|
||
await load();
|
||
}
|
||
|
||
/// Создать cron-задачу
|
||
Future<void> addCron({
|
||
required String targetId,
|
||
required String hour,
|
||
required String minute,
|
||
String dayOfWeek = '*',
|
||
bool isGroup = true,
|
||
bool targetState = true,
|
||
}) async {
|
||
await ref.read(apiProvider).scheduleCron({
|
||
'target_id': targetId,
|
||
'hour': hour,
|
||
'minute': minute,
|
||
'day_of_week': dayOfWeek,
|
||
'is_group': isGroup,
|
||
'state': targetState,
|
||
});
|
||
await load();
|
||
}
|
||
}
|