feat: type remote device models
This commit is contained in:
98
lib/models/ignis_device.dart
Normal file
98
lib/models/ignis_device.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
class IgnisDevice {
|
||||
final String id;
|
||||
final String? mac;
|
||||
final String name;
|
||||
final String? model;
|
||||
final String? ip;
|
||||
|
||||
const IgnisDevice({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.mac,
|
||||
this.model,
|
||||
this.ip,
|
||||
});
|
||||
|
||||
String get groupMemberId => mac ?? id;
|
||||
|
||||
String? get subtitle {
|
||||
final parts = <String>[?mac, ?ip];
|
||||
return parts.isEmpty ? null : parts.join(' - ');
|
||||
}
|
||||
|
||||
static IgnisDevice fromApi(Object? value, {String? fallbackId}) {
|
||||
if (value is Map) {
|
||||
final map = Map<String, dynamic>.from(value);
|
||||
final id =
|
||||
_stringValue(map, const ['id', 'mac', 'mac_address', 'device_id']) ??
|
||||
fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('device не содержит id/mac');
|
||||
}
|
||||
|
||||
final mac = _stringValue(map, const ['mac', 'mac_address']);
|
||||
final model = _stringValue(map, const ['model']);
|
||||
final name =
|
||||
_stringValue(map, const ['name', 'label', 'model']) ?? mac ?? id;
|
||||
final ip = _stringValue(map, const ['ip', 'address']);
|
||||
|
||||
return IgnisDevice(id: id, mac: mac, name: name, model: model, ip: ip);
|
||||
}
|
||||
|
||||
final id = value?.toString() ?? fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('device должен быть объектом или id');
|
||||
}
|
||||
return IgnisDevice(id: id, mac: id, name: id);
|
||||
}
|
||||
|
||||
static List<IgnisDevice> listFromApi(Object? data) {
|
||||
final values = _collectionValues(data, const ['data', 'devices']);
|
||||
return values
|
||||
.map(
|
||||
(value) => value.entryKey == null
|
||||
? IgnisDevice.fromApi(value.value)
|
||||
: IgnisDevice.fromApi(value.value, fallbackId: value.entryKey),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
String? _stringValue(Map<String, dynamic> map, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = map[key];
|
||||
if (value != null && value.toString().isNotEmpty) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<_CollectionValue> _collectionValues(Object? data, List<String> wrappers) {
|
||||
if (data is List) {
|
||||
return data.map((value) => _CollectionValue(value)).toList();
|
||||
}
|
||||
|
||||
if (data is Map) {
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
for (final wrapper in wrappers) {
|
||||
final value = map[wrapper];
|
||||
if (value is List) {
|
||||
return value.map((item) => _CollectionValue(item)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
return map.entries
|
||||
.map((entry) => _CollectionValue(entry.value, entryKey: entry.key))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw const FormatException('ожидался список или объект');
|
||||
}
|
||||
|
||||
class _CollectionValue {
|
||||
final Object? value;
|
||||
final String? entryKey;
|
||||
|
||||
const _CollectionValue(this.value, {this.entryKey});
|
||||
}
|
||||
203
lib/models/ignis_group.dart
Normal file
203
lib/models/ignis_group.dart
Normal file
@@ -0,0 +1,203 @@
|
||||
class IgnisGroup {
|
||||
final String id;
|
||||
final String name;
|
||||
final List<String> macs;
|
||||
final IgnisGroupState state;
|
||||
|
||||
const IgnisGroup({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.macs = const [],
|
||||
this.state = const IgnisGroupState(),
|
||||
});
|
||||
|
||||
IgnisGroup copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
List<String>? macs,
|
||||
IgnisGroupState? state,
|
||||
}) {
|
||||
return IgnisGroup(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
macs: macs ?? this.macs,
|
||||
state: state ?? this.state,
|
||||
);
|
||||
}
|
||||
|
||||
static IgnisGroup fromApi(Object? value, {String? fallbackId}) {
|
||||
if (value is! Map) {
|
||||
final id = value?.toString() ?? fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('group должен быть объектом или id');
|
||||
}
|
||||
return IgnisGroup(id: id, name: id);
|
||||
}
|
||||
|
||||
final map = Map<String, dynamic>.from(value);
|
||||
final id = _stringValue(map, const ['id', 'group_id']) ?? fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('group не содержит id');
|
||||
}
|
||||
|
||||
final name = _stringValue(map, const ['name', 'label']) ?? id;
|
||||
final macs = _stringList(
|
||||
map['macs'] ?? map['devices'] ?? map['device_ids'],
|
||||
);
|
||||
final state = IgnisGroupState.fromApi(
|
||||
map['last_state'] ?? map['state'] ?? map['status'],
|
||||
);
|
||||
|
||||
return IgnisGroup(id: id, name: name, macs: macs, state: state);
|
||||
}
|
||||
|
||||
static List<IgnisGroup> listFromApi(Object? data) {
|
||||
final values = _collectionValues(data, const ['data', 'groups']);
|
||||
return values.map((value) {
|
||||
if (value.entryKey == null) return IgnisGroup.fromApi(value.value);
|
||||
return IgnisGroup.fromApi(value.value, fallbackId: value.entryKey);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class IgnisGroupState {
|
||||
final bool isOn;
|
||||
final int brightness;
|
||||
final int temp;
|
||||
final int r;
|
||||
final int g;
|
||||
final int b;
|
||||
final String? sceneId;
|
||||
|
||||
const IgnisGroupState({
|
||||
this.isOn = false,
|
||||
this.brightness = 100,
|
||||
this.temp = 4000,
|
||||
this.r = 255,
|
||||
this.g = 200,
|
||||
this.b = 100,
|
||||
this.sceneId,
|
||||
});
|
||||
|
||||
IgnisGroupState copyWith({
|
||||
bool? isOn,
|
||||
int? brightness,
|
||||
int? temp,
|
||||
int? r,
|
||||
int? g,
|
||||
int? b,
|
||||
String? sceneId,
|
||||
}) {
|
||||
return IgnisGroupState(
|
||||
isOn: isOn ?? this.isOn,
|
||||
brightness: brightness ?? this.brightness,
|
||||
temp: temp ?? this.temp,
|
||||
r: r ?? this.r,
|
||||
g: g ?? this.g,
|
||||
b: b ?? this.b,
|
||||
sceneId: sceneId ?? this.sceneId,
|
||||
);
|
||||
}
|
||||
|
||||
IgnisGroupState applyPatch(Map<String, dynamic> patch) {
|
||||
return copyWith(
|
||||
isOn: patch.containsKey('state') ? patch['state'] == true : null,
|
||||
brightness: _intValue(patch['brightness'] ?? patch['dimming']),
|
||||
temp: _intValue(patch['temp']),
|
||||
r: _intValue(patch['r']),
|
||||
g: _intValue(patch['g']),
|
||||
b: _intValue(patch['b']),
|
||||
sceneId: patch['scene']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
static IgnisGroupState fromApi(Object? value, {IgnisGroupState? fallback}) {
|
||||
final base = fallback ?? const IgnisGroupState();
|
||||
if (value is! Map) return base;
|
||||
|
||||
final map = Map<String, dynamic>.from(value);
|
||||
return base.applyPatch({
|
||||
'state': map['state'],
|
||||
'brightness': map['brightness'] ?? map['dimming'],
|
||||
'temp': map['temp'],
|
||||
'r': map['r'],
|
||||
'g': map['g'],
|
||||
'b': map['b'],
|
||||
'scene': map['scene'],
|
||||
});
|
||||
}
|
||||
|
||||
static IgnisGroupState? firstFromStatusResponse(
|
||||
Object? data, {
|
||||
IgnisGroupState? fallback,
|
||||
}) {
|
||||
if (data is! Map) return fallback;
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
final results = map['results'];
|
||||
if (results is! List || results.isEmpty) return fallback;
|
||||
|
||||
Object? validResult;
|
||||
for (final result in results) {
|
||||
if (result is Map &&
|
||||
result['status'] != null &&
|
||||
result['error'] == null) {
|
||||
validResult = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
validResult ??= results.first;
|
||||
|
||||
if (validResult is! Map) return fallback;
|
||||
return IgnisGroupState.fromApi(validResult['status'], fallback: fallback);
|
||||
}
|
||||
}
|
||||
|
||||
String? _stringValue(Map<String, dynamic> map, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = map[key];
|
||||
if (value != null && value.toString().isNotEmpty) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? _intValue(Object? value) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
List<String> _stringList(Object? value) {
|
||||
if (value is! List) return const [];
|
||||
return value.map((item) => item.toString()).toList();
|
||||
}
|
||||
|
||||
List<_CollectionValue> _collectionValues(Object? data, List<String> wrappers) {
|
||||
if (data is List) {
|
||||
return data.map((value) => _CollectionValue(value)).toList();
|
||||
}
|
||||
|
||||
if (data is Map) {
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
for (final wrapper in wrappers) {
|
||||
final value = map[wrapper];
|
||||
if (value is List) {
|
||||
return value.map((item) => _CollectionValue(item)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
return map.entries
|
||||
.map((entry) => _CollectionValue(entry.value, entryKey: entry.key))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw const FormatException('ожидался список или объект');
|
||||
}
|
||||
|
||||
class _CollectionValue {
|
||||
final Object? value;
|
||||
final String? entryKey;
|
||||
|
||||
const _CollectionValue(this.value, {this.entryKey});
|
||||
}
|
||||
139
lib/models/ignis_scene.dart
Normal file
139
lib/models/ignis_scene.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
class IgnisScene {
|
||||
final String id;
|
||||
final String displayName;
|
||||
|
||||
const IgnisScene({required this.id, required this.displayName});
|
||||
|
||||
static IgnisScene fromApi(Object? value, {String? fallbackId}) {
|
||||
if (value is Map) {
|
||||
final map = Map<String, dynamic>.from(value);
|
||||
final id =
|
||||
_stringValue(map, const ['id', 'scene', 'scene_id', 'value']) ??
|
||||
fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('scene не содержит id');
|
||||
}
|
||||
|
||||
final explicitName = _stringValue(map, const [
|
||||
'name',
|
||||
'label',
|
||||
'display_name',
|
||||
'title',
|
||||
]);
|
||||
return IgnisScene(
|
||||
id: id,
|
||||
displayName: explicitName ?? displayNameFor(id),
|
||||
);
|
||||
}
|
||||
|
||||
final id = value?.toString() ?? fallbackId;
|
||||
if (id == null || id.isEmpty) {
|
||||
throw const FormatException('scene должен быть объектом или id');
|
||||
}
|
||||
return IgnisScene(id: id, displayName: displayNameFor(id));
|
||||
}
|
||||
|
||||
static List<IgnisScene> listFromApi(Object? data) {
|
||||
final values = _collectionValues(data, const ['data', 'scenes']);
|
||||
return values.map((value) {
|
||||
if (value.entryKey == null) {
|
||||
return IgnisScene.fromApi(value.value);
|
||||
}
|
||||
|
||||
if (value.value is String || value.value is num) {
|
||||
final name = value.value.toString();
|
||||
return IgnisScene(
|
||||
id: value.entryKey!,
|
||||
displayName: _looksLikeTechnicalId(name)
|
||||
? displayNameFor(name)
|
||||
: name,
|
||||
);
|
||||
}
|
||||
|
||||
return IgnisScene.fromApi(value.value, fallbackId: value.entryKey);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static String displayNameFor(String id) {
|
||||
final normalized = id.trim();
|
||||
final knownName = _wizSceneNames[normalized];
|
||||
if (knownName != null) return knownName;
|
||||
return 'Сцена $normalized';
|
||||
}
|
||||
}
|
||||
|
||||
const Map<String, String> _wizSceneNames = {
|
||||
'1': 'Океан',
|
||||
'2': 'Романтика',
|
||||
'3': 'Закат',
|
||||
'4': 'Вечеринка',
|
||||
'5': 'Камин',
|
||||
'6': 'Уют',
|
||||
'7': 'Лес',
|
||||
'8': 'Пастель',
|
||||
'9': 'Пробуждение',
|
||||
'10': 'Сон',
|
||||
'11': 'Тёплый белый',
|
||||
'12': 'Дневной свет',
|
||||
'13': 'Холодный белый',
|
||||
'14': 'Ночник',
|
||||
'15': 'Фокус',
|
||||
'16': 'Расслабление',
|
||||
'17': 'Настоящие цвета',
|
||||
'18': 'ТВ',
|
||||
'19': 'Рост растений',
|
||||
'20': 'Весна',
|
||||
'21': 'Лето',
|
||||
'22': 'Осень',
|
||||
'23': 'Погружение',
|
||||
'24': 'Джунгли',
|
||||
'25': 'Мохито',
|
||||
'26': 'Клуб',
|
||||
'27': 'Рождество',
|
||||
'28': 'Хэллоуин',
|
||||
'29': 'Свеча',
|
||||
'30': 'Золотистый белый',
|
||||
'31': 'Пульс',
|
||||
'32': 'Стимпанк',
|
||||
};
|
||||
|
||||
String? _stringValue(Map<String, dynamic> map, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = map[key];
|
||||
if (value != null && value.toString().isNotEmpty) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _looksLikeTechnicalId(String value) => int.tryParse(value.trim()) != null;
|
||||
|
||||
List<_CollectionValue> _collectionValues(Object? data, List<String> wrappers) {
|
||||
if (data is List) {
|
||||
return data.map((value) => _CollectionValue(value)).toList();
|
||||
}
|
||||
|
||||
if (data is Map) {
|
||||
final map = Map<String, dynamic>.from(data);
|
||||
for (final wrapper in wrappers) {
|
||||
final value = map[wrapper];
|
||||
if (value is List) {
|
||||
return value.map((item) => _CollectionValue(item)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
return map.entries
|
||||
.map((entry) => _CollectionValue(entry.value, entryKey: entry.key))
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw const FormatException('ожидался список или объект');
|
||||
}
|
||||
|
||||
class _CollectionValue {
|
||||
final Object? value;
|
||||
final String? entryKey;
|
||||
|
||||
const _CollectionValue(this.value, {this.entryKey});
|
||||
}
|
||||
Reference in New Issue
Block a user