feat: secure home credentials
This commit is contained in:
@@ -20,8 +20,9 @@ final apiProvider = Provider((ref) => IgnisApi());
|
||||
// ─── Текущий дом ─────────────────────────────────────────────
|
||||
|
||||
/// Текущий выбранный дом (null если ни одного нет)
|
||||
final currentHomeProvider =
|
||||
NotifierProvider<CurrentHomeNotifier, HomeConfig?>(() => CurrentHomeNotifier());
|
||||
final currentHomeProvider = NotifierProvider<CurrentHomeNotifier, HomeConfig?>(
|
||||
() => CurrentHomeNotifier(),
|
||||
);
|
||||
|
||||
class CurrentHomeNotifier extends Notifier<HomeConfig?> {
|
||||
@override
|
||||
@@ -32,7 +33,7 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
|
||||
final svc = ref.read(settingsServiceProvider);
|
||||
state = await svc.getCurrentHome();
|
||||
if (state != null) {
|
||||
_initApi(state!);
|
||||
await _initApi(state!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,21 +42,25 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
|
||||
final svc = ref.read(settingsServiceProvider);
|
||||
await svc.setCurrentHomeId(home.id);
|
||||
state = home;
|
||||
_initApi(home);
|
||||
await _initApi(home);
|
||||
// Перезагрузить группы для нового дома
|
||||
await ref.read(groupsProvider.notifier).initAndRefresh();
|
||||
}
|
||||
|
||||
/// Инициализировать API-клиент текущим домом
|
||||
void _initApi(HomeConfig home) {
|
||||
ref.read(apiProvider).init(home.url, home.apiKey);
|
||||
Future<void> _initApi(HomeConfig home) async {
|
||||
final apiKey = await ref
|
||||
.read(settingsServiceProvider)
|
||||
.requireHomeApiKey(home.id);
|
||||
ref.read(apiProvider).init(home.url, apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Список домов ────────────────────────────────────────────
|
||||
|
||||
final homesProvider =
|
||||
NotifierProvider<HomesNotifier, List<HomeConfig>>(() => HomesNotifier());
|
||||
final homesProvider = NotifierProvider<HomesNotifier, List<HomeConfig>>(
|
||||
() => HomesNotifier(),
|
||||
);
|
||||
|
||||
class HomesNotifier extends Notifier<List<HomeConfig>> {
|
||||
@override
|
||||
@@ -65,8 +70,8 @@ class HomesNotifier extends Notifier<List<HomeConfig>> {
|
||||
state = await ref.read(settingsServiceProvider).getHomes();
|
||||
}
|
||||
|
||||
Future<void> add(HomeConfig home) async {
|
||||
await ref.read(settingsServiceProvider).upsertHome(home);
|
||||
Future<void> add(HomeConfig home, {required String apiKey}) async {
|
||||
await ref.read(settingsServiceProvider).upsertHome(home, apiKey: apiKey);
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -75,8 +80,8 @@ class HomesNotifier extends Notifier<List<HomeConfig>> {
|
||||
await load();
|
||||
}
|
||||
|
||||
Future<void> update(HomeConfig home) async {
|
||||
await ref.read(settingsServiceProvider).upsertHome(home);
|
||||
Future<void> update(HomeConfig home, {String? apiKey}) async {
|
||||
await ref.read(settingsServiceProvider).upsertHome(home, apiKey: apiKey);
|
||||
await load();
|
||||
}
|
||||
}
|
||||
@@ -98,14 +103,18 @@ class UserLocation {
|
||||
double? distanceToKm(double? lat, double? lon) {
|
||||
if (position == null || lat == null || lon == null) return null;
|
||||
return calculateDistanceKm(
|
||||
position!.latitude, position!.longitude, lat, lon,
|
||||
position!.latitude,
|
||||
position!.longitude,
|
||||
lat,
|
||||
lon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final userLocationProvider =
|
||||
NotifierProvider<UserLocationNotifier, UserLocation>(
|
||||
() => UserLocationNotifier());
|
||||
() => UserLocationNotifier(),
|
||||
);
|
||||
|
||||
class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
StreamSubscription<Position>? _sub;
|
||||
@@ -221,8 +230,9 @@ class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
|
||||
// ─── Группы текущего дома ────────────────────────────────────
|
||||
|
||||
final groupsProvider =
|
||||
NotifierProvider<GroupsNotifier, List<dynamic>>(() => GroupsNotifier());
|
||||
final groupsProvider = NotifierProvider<GroupsNotifier, List<dynamic>>(
|
||||
() => GroupsNotifier(),
|
||||
);
|
||||
|
||||
class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
IgnisApi get _api => ref.read(apiProvider);
|
||||
@@ -250,7 +260,10 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
Future<void> initAndRefresh() async {
|
||||
final home = ref.read(currentHomeProvider);
|
||||
if (home == null) return;
|
||||
_api.init(home.url, home.apiKey);
|
||||
final apiKey = await ref
|
||||
.read(settingsServiceProvider)
|
||||
.requireHomeApiKey(home.id);
|
||||
_api.init(home.url, apiKey);
|
||||
await refresh();
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 10), (_) => refresh());
|
||||
@@ -272,55 +285,58 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
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();
|
||||
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.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 results = resStatus.data['results'] as List;
|
||||
final validResult = results.firstWhere(
|
||||
(r) => r['status'] != null && r['error'] == null,
|
||||
orElse: () => results.first,
|
||||
// Если группа залочена (недавно управляли) -- берём локальное состояние
|
||||
if (_lockUntil.containsKey(id) && _lockUntil[id]!.isAfter(now)) {
|
||||
final existing = state.firstWhere(
|
||||
(old) => old['id'].toString() == id,
|
||||
orElse: () => null,
|
||||
);
|
||||
final data = validResult['status'];
|
||||
if (data != null) {
|
||||
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'],
|
||||
};
|
||||
}
|
||||
return existing ?? map;
|
||||
}
|
||||
} catch (e) {
|
||||
// При ошибке опроса -- сохраняем предыдущее состояние
|
||||
final existing = state.firstWhere(
|
||||
(s) => s['id'].toString() == id,
|
||||
orElse: () => null,
|
||||
);
|
||||
map['last_state'] = existing?['last_state'] ??
|
||||
{'state': false, 'brightness': 100, 'temp': 4000};
|
||||
}
|
||||
return 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 results = resStatus.data['results'] as List;
|
||||
final validResult = results.firstWhere(
|
||||
(r) => r['status'] != null && r['error'] == null,
|
||||
orElse: () => results.first,
|
||||
);
|
||||
final data = validResult['status'];
|
||||
if (data != null) {
|
||||
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.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) {
|
||||
@@ -341,19 +357,23 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
...g,
|
||||
'last_state': {
|
||||
...Map<String, dynamic>.from(g['last_state'] ?? {}),
|
||||
...patch
|
||||
}
|
||||
...patch,
|
||||
},
|
||||
}
|
||||
else
|
||||
g
|
||||
g,
|
||||
];
|
||||
}
|
||||
|
||||
/// Debounce: отправить API-запрос с задержкой, но UI обновить сразу.
|
||||
/// Если значение меняется быстро (слайдер тянут), отправляется только
|
||||
/// последнее значение после паузы.
|
||||
void _debouncedControl(String id, String key, Map<String, dynamic> localPatch,
|
||||
Map<String, dynamic> apiParams) {
|
||||
void _debouncedControl(
|
||||
String id,
|
||||
String key,
|
||||
Map<String, dynamic> localPatch,
|
||||
Map<String, dynamic> apiParams,
|
||||
) {
|
||||
_setLock(id);
|
||||
_updateLocal(id, localPatch);
|
||||
|
||||
@@ -386,7 +406,12 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
/// Установить яркость (0-100) -- с debounce
|
||||
void setBrightness(String id, int value) {
|
||||
_debouncedControl(id, 'brightness', {'brightness': value}, {'brightness': value});
|
||||
_debouncedControl(
|
||||
id,
|
||||
'brightness',
|
||||
{'brightness': value},
|
||||
{'brightness': value},
|
||||
);
|
||||
}
|
||||
|
||||
/// Установить цветовую температуру (2700-6500K) -- с debounce
|
||||
@@ -396,7 +421,12 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
/// Установить RGB-цвет -- с debounce
|
||||
void setColor(String id, int r, int g, int b) {
|
||||
_debouncedControl(id, 'color', {'r': r, 'g': g, 'b': b}, {'r': r, 'g': g, 'b': b});
|
||||
_debouncedControl(
|
||||
id,
|
||||
'color',
|
||||
{'r': r, 'g': g, 'b': b},
|
||||
{'r': r, 'g': g, 'b': b},
|
||||
);
|
||||
}
|
||||
|
||||
/// Установить сцену (без debounce)
|
||||
@@ -425,8 +455,9 @@ class GroupsNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
// ─── Устройства (для создания групп) ─────────────────────────
|
||||
|
||||
final devicesProvider =
|
||||
NotifierProvider<DevicesNotifier, List<dynamic>>(() => DevicesNotifier());
|
||||
final devicesProvider = NotifierProvider<DevicesNotifier, List<dynamic>>(
|
||||
() => DevicesNotifier(),
|
||||
);
|
||||
|
||||
class DevicesNotifier extends Notifier<List<dynamic>> {
|
||||
@override
|
||||
@@ -440,7 +471,8 @@ class DevicesNotifier extends Notifier<List<dynamic>> {
|
||||
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();
|
||||
state =
|
||||
res.data['data'] ?? res.data['devices'] ?? res.data.values.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Ошибка загрузки устройств: $e");
|
||||
@@ -450,8 +482,9 @@ class DevicesNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
// ─── Сцены ───────────────────────────────────────────────────
|
||||
|
||||
final scenesProvider =
|
||||
NotifierProvider<ScenesNotifier, List<dynamic>>(() => ScenesNotifier());
|
||||
final scenesProvider = NotifierProvider<ScenesNotifier, List<dynamic>>(
|
||||
() => ScenesNotifier(),
|
||||
);
|
||||
|
||||
class ScenesNotifier extends Notifier<List<dynamic>> {
|
||||
@override
|
||||
@@ -486,8 +519,9 @@ class ScenesNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
// ─── Расписания ──────────────────────────────────────────────
|
||||
|
||||
final tasksProvider =
|
||||
NotifierProvider<TasksNotifier, List<dynamic>>(() => TasksNotifier());
|
||||
final tasksProvider = NotifierProvider<TasksNotifier, List<dynamic>>(
|
||||
() => TasksNotifier(),
|
||||
);
|
||||
|
||||
class TasksNotifier extends Notifier<List<dynamic>> {
|
||||
@override
|
||||
@@ -559,8 +593,9 @@ class TasksNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
// ─── Статистика ──────────────────────────────────────────────
|
||||
|
||||
final statsProvider =
|
||||
NotifierProvider<StatsNotifier, Map<String, dynamic>>(() => StatsNotifier());
|
||||
final statsProvider = NotifierProvider<StatsNotifier, Map<String, dynamic>>(
|
||||
() => StatsNotifier(),
|
||||
);
|
||||
|
||||
class StatsNotifier extends Notifier<Map<String, dynamic>> {
|
||||
@override
|
||||
@@ -582,8 +617,9 @@ class StatsNotifier extends Notifier<Map<String, dynamic>> {
|
||||
|
||||
// ─── Лог событий ─────────────────────────────────────────────
|
||||
|
||||
final eventLogProvider =
|
||||
NotifierProvider<EventLogNotifier, List<dynamic>>(() => EventLogNotifier());
|
||||
final eventLogProvider = NotifierProvider<EventLogNotifier, List<dynamic>>(
|
||||
() => EventLogNotifier(),
|
||||
);
|
||||
|
||||
class EventLogNotifier extends Notifier<List<dynamic>> {
|
||||
@override
|
||||
@@ -607,8 +643,9 @@ class EventLogNotifier extends Notifier<List<dynamic>> {
|
||||
|
||||
// ─── API-ключи ───────────────────────────────────────────────
|
||||
|
||||
final apiKeysProvider =
|
||||
NotifierProvider<ApiKeysNotifier, List<dynamic>>(() => ApiKeysNotifier());
|
||||
final apiKeysProvider = NotifierProvider<ApiKeysNotifier, List<dynamic>>(
|
||||
() => ApiKeysNotifier(),
|
||||
);
|
||||
|
||||
class ApiKeysNotifier extends Notifier<List<dynamic>> {
|
||||
@override
|
||||
@@ -666,7 +703,9 @@ class ApiKeysNotifier extends Notifier<List<dynamic>> {
|
||||
// ─── Информация об авторизации ────────────────────────────────
|
||||
|
||||
final authInfoProvider =
|
||||
NotifierProvider<AuthInfoNotifier, Map<String, dynamic>?>(() => AuthInfoNotifier());
|
||||
NotifierProvider<AuthInfoNotifier, Map<String, dynamic>?>(
|
||||
() => AuthInfoNotifier(),
|
||||
);
|
||||
|
||||
class AuthInfoNotifier extends Notifier<Map<String, dynamic>?> {
|
||||
@override
|
||||
@@ -706,9 +745,7 @@ Future<void> syncGeofenceTask(List<HomeConfig> homes) async {
|
||||
geofenceTaskUniqueName,
|
||||
geofenceTaskName,
|
||||
frequency: const Duration(minutes: 15),
|
||||
constraints: Constraints(
|
||||
networkType: NetworkType.connected,
|
||||
),
|
||||
constraints: Constraints(networkType: NetworkType.connected),
|
||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.replace,
|
||||
backoffPolicy: BackoffPolicy.linear,
|
||||
backoffPolicyDelay: const Duration(minutes: 1),
|
||||
@@ -720,12 +757,12 @@ Future<void> syncGeofenceTask(List<HomeConfig> homes) async {
|
||||
|
||||
// ─── Утилита: расчёт расстояния (Haversine) ──────────────────
|
||||
|
||||
double calculateDistanceKm(
|
||||
double lat1, double lon1, double lat2, double lon2) {
|
||||
double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const earthRadiusKm = 6371.0;
|
||||
final dLat = _degToRad(lat2 - lat1);
|
||||
final dLon = _degToRad(lon2 - lon1);
|
||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
final a =
|
||||
math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(_degToRad(lat1)) *
|
||||
math.cos(_degToRad(lat2)) *
|
||||
math.sin(dLon / 2) *
|
||||
|
||||
Reference in New Issue
Block a user