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()); class CurrentHomeNotifier extends Notifier { @override HomeConfig? build() => null; /// Загрузить текущий дом из SharedPreferences Future load() async { final svc = ref.read(settingsServiceProvider); state = await svc.getCurrentHome(); if (state != null) { _initApi(state!); } } /// Переключиться на другой дом Future 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()); class HomesNotifier extends Notifier> { @override List build() => []; Future load() async { state = await ref.read(settingsServiceProvider).getHomes(); } Future add(HomeConfig home) async { await ref.read(settingsServiceProvider).upsertHome(home); await load(); } Future remove(String id) async { await ref.read(settingsServiceProvider).deleteHome(id); await load(); } Future update(HomeConfig home) async { await ref.read(settingsServiceProvider).upsertHome(home); await load(); } } // ─── Группы текущего дома ──────────────────────────────────── final groupsProvider = NotifierProvider>(() => GroupsNotifier()); class GroupsNotifier extends Notifier> { IgnisApi get _api => ref.read(apiProvider); Timer? _timer; /// Блокировка обновления для группы после управления -- /// чтобы UI не прыгал пока лампа ещё не ответила. final Map _lockUntil = {}; @override List build() { ref.onDispose(() => _timer?.cancel()); return []; } /// Инициализация: настроить API и начать периодический опрос Future 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 refresh() async { try { final resGroups = await _api.getGroups(); List 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.from(g); final id = map['id'].toString(); // Если группа залочена (недавно управляли) -- берём локальное состояние if (_lockUntil.containsKey(id) && _lockUntil[id]!.isAfter(now)) { final existing = state.cast().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().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 patch) { state = [ for (final g in state) if (g['id'].toString() == id) { ...g, 'last_state': { ...Map.from(g['last_state'] ?? {}), ...patch } } else g ]; } /// Включить/выключить группу Future 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 setBrightness(String id, int value) async { _setLock(id); _updateLocal(id, {'brightness': value}); await _api.controlGroup(id, {'brightness': value}); } /// Установить цветовую температуру (2700-6500K) Future setTemperature(String id, int value) async { _setLock(id); _updateLocal(id, {'temp': value}); await _api.controlGroup(id, {'temp': value}); } /// Установить RGB-цвет Future 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 setScene(String id, String scene) async { _setLock(id); _updateLocal(id, {'scene': scene}); await _api.controlGroup(id, {'scene': scene}); } /// Таймер: включить на 4 часа Future 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()); class DevicesNotifier extends Notifier> { @override List build() => []; /// Загрузить список устройств из текущего дома Future 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()); class ScenesNotifier extends Notifier> { @override List build() => []; Future 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()); class TasksNotifier extends Notifier> { @override List build() => []; Future 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 cancel(String jobId) async { try { await ref.read(apiProvider).cancelTask(jobId); await load(); } catch (e) { debugPrint("Ошибка отмены задачи: $e"); } } /// Создать одноразовый таймер Future addOnce({ required String targetId, required bool targetState, int? hoursFromNow, String? runAt, bool isGroup = true, }) async { final params = { '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 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(); } }