Каркас приложения: авторизация, зоны/устройства/правила, управление устройствами

HTTP-клиент к /api/v1 с Sanctum-токеном (хранится в secure storage),
модели зеркалят Laravel API Resources. Экраны: логин, список устройств
с живым статусом, детальная страница устройства (история телеметрии,
turn-on/turn-off/set-level с проверкой capability), списки зон и правил
автоматизации (пока только чтение — формы создания/редактирования впереди).
This commit is contained in:
2026-08-12 01:12:39 +05:00
parent 6a00826194
commit 45b8733d22
25 changed files with 1531 additions and 129 deletions
+47
View File
@@ -0,0 +1,47 @@
import 'device.dart';
import 'zone.dart';
class AutomationRule {
AutomationRule({
required this.id,
required this.conditionSensorType,
required this.conditionOperator,
required this.conditionValue,
required this.actionType,
required this.actionParams,
required this.isActive,
this.zone,
this.targetDevice,
this.conditionSourceDevice,
});
final int id;
final Zone? zone;
final Device? targetDevice;
final Device? conditionSourceDevice;
final String conditionSensorType;
final String conditionOperator; // ">", "<", ">=", "<=", "=="
final double conditionValue;
final String actionType; // "turn_on", "turn_off", "set_level", ...
final Map<String, dynamic> actionParams;
final bool isActive;
/// `action_params.level` for `set_level` rules, if present.
num? get level => actionParams['level'] as num?;
factory AutomationRule.fromJson(Map<String, dynamic> json) => AutomationRule(
id: json['id'] as int,
zone: json['zone'] is Map ? Zone.fromJson(json['zone'] as Map<String, dynamic>) : null,
targetDevice:
json['target_device'] is Map ? Device.fromJson(json['target_device'] as Map<String, dynamic>) : null,
conditionSourceDevice: json['condition_source_device'] is Map
? Device.fromJson(json['condition_source_device'] as Map<String, dynamic>)
: null,
conditionSensorType: json['condition_sensor_type'] as String,
conditionOperator: json['condition_operator'] as String,
conditionValue: (json['condition_value'] as num).toDouble(),
actionType: json['action_type'] as String,
actionParams: Map<String, dynamic>.from(json['action_params'] as Map? ?? const {}),
isActive: json['is_active'] as bool? ?? false,
);
}
+80
View File
@@ -0,0 +1,80 @@
import 'device_type.dart';
import 'zone.dart';
class Device {
Device({
required this.id,
required this.name,
required this.externalId,
required this.protocol,
required this.status,
this.zone,
this.deviceType,
});
final int id;
final String name;
final String externalId;
final String protocol;
final String status; // "online" | "offline" | "unknown"
final Zone? zone;
final DeviceType? deviceType;
factory Device.fromJson(Map<String, dynamic> json) => Device(
id: json['id'] as int,
name: json['name'] as String,
externalId: json['external_id'] as String,
protocol: json['protocol'] as String,
status: json['status'] as String? ?? 'unknown',
zone: json['zone'] is Map ? Zone.fromJson(json['zone'] as Map<String, dynamic>) : null,
deviceType: json['device_type'] is Map
? DeviceType.fromJson(json['device_type'] as Map<String, dynamic>)
: null,
);
}
/// The extra data returned only by `GET /devices/{id}`: Device Shadow
/// snapshot (Redis) plus recent telemetry readings (ClickHouse, sensors only).
class DeviceDetail {
DeviceDetail({
required this.device,
required this.lastSeen,
required this.desiredState,
required this.reportedState,
required this.telemetry,
});
final Device device;
final DateTime? lastSeen;
final Map<String, dynamic> desiredState;
final Map<String, dynamic> reportedState;
final List<TelemetryReading> telemetry;
factory DeviceDetail.fromJson(Map<String, dynamic> json) {
final shadow = json['shadow'] as Map<String, dynamic>;
return DeviceDetail(
device: Device.fromJson(json['data'] as Map<String, dynamic>),
lastSeen: shadow['last_seen'] != null ? DateTime.tryParse(shadow['last_seen'] as String) : null,
desiredState: Map<String, dynamic>.from(shadow['desired_state'] as Map? ?? const {}),
reportedState: Map<String, dynamic>.from(shadow['reported_state'] as Map? ?? const {}),
telemetry: (json['telemetry'] as List? ?? const [])
.map((e) => TelemetryReading.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}
class TelemetryReading {
TelemetryReading({required this.sensorType, required this.value, required this.recordedAt});
final String sensorType;
final double value;
final String recordedAt;
factory TelemetryReading.fromJson(Map<String, dynamic> json) => TelemetryReading(
sensorType: json['sensor_type'] as String,
value: (json['value'] as num).toDouble(),
recordedAt: json['recorded_at'] as String,
);
}
+25
View File
@@ -0,0 +1,25 @@
class DeviceType {
DeviceType({
required this.id,
required this.code,
required this.category,
required this.capabilities,
});
final int id;
final String code;
final String category; // "actuator" | "sensor"
final List<String> capabilities;
bool get isSensor => category == 'sensor';
bool get isActuator => category == 'actuator';
bool supports(String capability) => capabilities.contains(capability);
factory DeviceType.fromJson(Map<String, dynamic> json) => DeviceType(
id: json['id'] as int,
code: json['code'] as String,
category: json['category'] as String,
capabilities: List<String>.from(json['capabilities'] as List? ?? const []),
);
}
+17
View File
@@ -0,0 +1,17 @@
class User {
User({required this.id, required this.name, required this.email, required this.role});
final int id;
final String name;
final String email;
final String role; // "owner" | "viewer"
bool get isOwner => role == 'owner';
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
role: json['role'] as String,
);
}
+15
View File
@@ -0,0 +1,15 @@
class Zone {
Zone({required this.id, required this.name, this.description, this.devicesCount});
final int id;
final String name;
final String? description;
final int? devicesCount;
factory Zone.fromJson(Map<String, dynamic> json) => Zone(
id: json['id'] as int,
name: json['name'] as String,
description: json['description'] as String?,
devicesCount: json['devices_count'] as int?,
);
}