fix: report group control failures

This commit is contained in:
Artem Kokos
2026-04-23 20:32:44 +07:00
parent 1c40852ac6
commit 736a61d54b
4 changed files with 93 additions and 15 deletions

View File

@@ -278,7 +278,7 @@ class GroupsLoadStateNotifier extends Notifier<GroupsLoadState> {
} }
void setError(Object error) => void setError(Object error) =>
state = GroupsLoadState.error(error.toString()); state = GroupsLoadState.error(describeLoadError(error));
} }
class GroupsNotifier extends Notifier<List<dynamic>> { class GroupsNotifier extends Notifier<List<dynamic>> {
@@ -432,7 +432,6 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
state = updatedList; state = updatedList;
ref.read(groupsLoadStateProvider.notifier).setData(updatedList); ref.read(groupsLoadStateProvider.notifier).setData(updatedList);
} catch (e) { } catch (e) {
debugPrint("Ошибка глобального опроса: $e");
if (_isActiveGeneration(generation)) { if (_isActiveGeneration(generation)) {
ref.read(groupsLoadStateProvider.notifier).setError(e); ref.read(groupsLoadStateProvider.notifier).setError(e);
} }
@@ -474,6 +473,7 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
void _debouncedControl( void _debouncedControl(
String id, String id,
String key, String key,
String action,
Map<String, dynamic> localPatch, Map<String, dynamic> localPatch,
Map<String, dynamic> apiParams, Map<String, dynamic> apiParams,
) { ) {
@@ -489,7 +489,8 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
await _api.controlGroup(id, apiParams); await _api.controlGroup(id, apiParams);
} catch (e) { } catch (e) {
_lockUntil.remove(id); _lockUntil.remove(id);
refresh(); await refresh();
ref.read(groupControlErrorProvider.notifier).report(id, action, e);
} }
}, },
); );
@@ -513,6 +514,7 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
_debouncedControl( _debouncedControl(
id, id,
'brightness', 'brightness',
'яркость',
{'brightness': value}, {'brightness': value},
{'brightness': value}, {'brightness': value},
); );
@@ -520,7 +522,13 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
/// Установить цветовую температуру (2700-6500K) -- с debounce /// Установить цветовую температуру (2700-6500K) -- с debounce
void setTemperature(String id, int value) { void setTemperature(String id, int value) {
_debouncedControl(id, 'temp', {'temp': value}, {'temp': value}); _debouncedControl(
id,
'temp',
'температуру',
{'temp': value},
{'temp': value},
);
} }
/// Установить RGB-цвет -- с debounce /// Установить RGB-цвет -- с debounce
@@ -528,6 +536,7 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
_debouncedControl( _debouncedControl(
id, id,
'color', 'color',
'цвет',
{'r': r, 'g': g, 'b': b}, {'r': r, 'g': g, 'b': b},
{'r': r, 'g': g, 'b': b}, {'r': r, 'g': g, 'b': b},
); );
@@ -558,6 +567,41 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
} }
} }
class GroupControlError {
final String groupId;
final String action;
final String message;
final int sequence;
const GroupControlError({
required this.groupId,
required this.action,
required this.message,
required this.sequence,
});
}
final groupControlErrorProvider =
NotifierProvider<GroupControlErrorNotifier, GroupControlError?>(
() => GroupControlErrorNotifier(),
);
class GroupControlErrorNotifier extends Notifier<GroupControlError?> {
int _sequence = 0;
@override
GroupControlError? build() => null;
void report(String groupId, String action, Object error) {
state = GroupControlError(
groupId: groupId,
action: action,
message: describeLoadError(error),
sequence: ++_sequence,
);
}
}
// ─── Устройства (для создания групп) ───────────────────────── // ─── Устройства (для создания групп) ─────────────────────────
final devicesProvider = final devicesProvider =

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/error_message.dart';
import '../providers/providers.dart'; import '../providers/providers.dart';
import '../widgets/group_card.dart'; import '../widgets/group_card.dart';
import 'homes_screen.dart'; import 'homes_screen.dart';
@@ -191,8 +192,7 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
), ),
child: const Icon(Icons.delete, color: Colors.redAccent), child: const Icon(Icons.delete, color: Colors.redAccent),
), ),
confirmDismiss: (_) => _confirmDeleteGroup(context, g), confirmDismiss: (_) => _confirmAndDeleteGroup(context, g),
onDismissed: (_) => _deleteGroup(g['id'].toString()),
child: GroupCard(group: g), child: GroupCard(group: g),
); );
}, },
@@ -201,12 +201,13 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
); );
} }
/// Подтверждение удаления группы свайпом Future<bool> _confirmAndDeleteGroup(
Future<bool> _confirmDeleteGroup(
BuildContext context, BuildContext context,
Map<String, dynamic> g, Map<String, dynamic> g,
) async { ) async {
return await showDialog<bool>( final messenger = ScaffoldMessenger.of(context);
final confirmed =
await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Удалить группу?'), title: const Text('Удалить группу?'),
@@ -227,18 +228,20 @@ class _RemoteScreenState extends ConsumerState<RemoteScreen> {
), ),
) ?? ) ??
false; false;
}
Future<void> _deleteGroup(String id) async { if (!confirmed) return false;
try { try {
await ref.read(apiProvider).deleteGroup(id); await ref.read(apiProvider).deleteGroup(g['id'].toString());
await ref.read(groupsProvider.notifier).refresh(); await ref.read(groupsProvider.notifier).refresh();
return true;
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( messenger.showSnackBar(
context, SnackBar(content: Text('Ошибка удаления: ${describeLoadError(e)}')),
).showSnackBar(SnackBar(content: Text('Ошибка удаления: $e'))); );
} }
return false;
} }
} }
} }

View File

@@ -38,6 +38,17 @@ class _GroupCardState extends ConsumerState<GroupCard> {
final int gVal = g['last_state']?['g'] ?? 200; final int gVal = g['last_state']?['g'] ?? 200;
final int b = g['last_state']?['b'] ?? 100; final int b = g['last_state']?['b'] ?? 100;
ref.listen<GroupControlError?>(groupControlErrorProvider, (previous, next) {
if (next == null || next.groupId != id) return;
if (previous?.sequence == next.sequence) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Не удалось применить ${next.action}: ${next.message}'),
),
);
});
// Значения слайдеров: локальные (если тянем) или серверные // Значения слайдеров: локальные (если тянем) или серверные
final briValue = (_localBrightness ?? bri.toDouble()).clamp(10.0, 100.0); final briValue = (_localBrightness ?? bri.toDouble()).clamp(10.0, 100.0);
final tempValue = (_localTemp ?? temp.toDouble()).clamp(2700.0, 6500.0); final tempValue = (_localTemp ?? temp.toDouble()).clamp(2700.0, 6500.0);

View File

@@ -478,4 +478,24 @@ void main() {
expect(api.controlledGroupId, 'kitchen'); expect(api.controlledGroupId, 'kitchen');
expect(api.controlGroupParams, containsPair('scene', 'party')); expect(api.controlGroupParams, containsPair('scene', 'party'));
}); });
test('debounced group control error is reported', () async {
final api = FakeIgnisApi();
final container = containerWith(api);
api.controlGroupError = DioException(
requestOptions: RequestOptions(path: '/control/group/kitchen'),
type: DioExceptionType.connectionError,
message: 'No route to host',
);
container.read(groupsProvider.notifier).setBrightness('kitchen', 42);
await Future<void>.delayed(const Duration(milliseconds: 400));
final error = container.read(groupControlErrorProvider);
expect(error, isNotNull);
expect(error!.groupId, 'kitchen');
expect(error.action, 'яркость');
expect(error.message, contains('Backend недоступен'));
expect(api.controlGroupParams, containsPair('brightness', 42));
});
} }