Files
ignis_app/lib/providers/providers.dart

366 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}