feat: ignis app v1.0.0 -- управление WiZ лампами
This commit is contained in:
365
lib/providers/providers.dart
Normal file
365
lib/providers/providers.dart
Normal file
@@ -0,0 +1,365 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user