Каркас приложения: авторизация, зоны/устройства/правила, управление устройствами
HTTP-клиент к /api/v1 с Sanctum-токеном (хранится в secure storage), модели зеркалят Laravel API Resources. Экраны: логин, список устройств с живым статусом, детальная страница устройства (история телеметрии, turn-on/turn-off/set-level с проверкой capability), списки зон и правил автоматизации (пока только чтение — формы создания/редактирования впереди).
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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 []),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user