fix: report group control failures
This commit is contained in:
@@ -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 =
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user