Lot Connexions multi-HEMS : - modèle Installation + InstallationStore (persistance par UUID ; métadonnées SharedPreferences, token via flutter_secure_storage) - ConnectionManager au-dessus de nymea_service : mono-connexion, switchTo, connectNew, enterDemo (démo = active factice), contrôle d'identité DHCP - nymea_service : connect() réveillé (ws://4444, token par UUID), capture Hello (uuid/name/initialSetupRequired), authenticate/createUser, resetState - écran Installations (3 états) + déverrouillage installateur depuis la racine - écran Connexion : auth 2 branches (JSONRPC.Authenticate / CreateUser), détection auto via initialSetupRequired, segmenteur de repli - gate de routage (redirect + refreshListenable merge[cm,svc]) ; hasActiveConnection = isSimulation || (connected && authenticated) - en-tête drawer cliquable → Installations - invalidation des caches au switch (service.resetState + clearForSwitch rôles/scheduler/tariff), sans persist() Lot Rôles & appareils : - EnergySetupProvider refondu en tri-état (present/absent/non-configuré) - écran roles_devices_screen (3 zones, drag-priorité, inférence solaire/batterie) - LoadDescriptor (etmvariableload, rév.2 : PAC exclue → SG-Ready) ; Energy.SetRootMeter réel, Get/SetLoadConfig en stub loggé Correctifs : - bug signe énergie : production +, consommation NÉGATIVE sur nymea 1.15.2 → consumptionW/home en .abs() (autoconso n'est plus clouée à 0) - UUID Hello brace-wrapped normalisé Tests : flutter analyze 0 erreur ; gate + zéro-fuite au switch (5/5 verts). Validé en vrai : login auth .120, .75 ouvert, switch, déverrouillage installateur. Note : embarque aussi le WIP dashboard/thème déjà présent dans l'arbre. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
488 lines
20 KiB
Dart
488 lines
20 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import '../services/nymea_service.dart';
|
|
import '../models/nymea_models.dart';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Rôles EMS
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Rôles de l'EMS (Energy Management System).
|
|
///
|
|
/// Six rôles répartis en trois destinations (contrat) :
|
|
/// • Compteurs → [gridMeter] (rootMeter), [solarMeter] (détecté)
|
|
/// • Charges pilotables → [evCharger], [dhw], [heatPump], [battery]
|
|
/// • Absent → n'importe quel rôle acté « Pas d'appareil »
|
|
enum EmsRole {
|
|
gridMeter, // Compteur réseau → Energy.SetRootMeter
|
|
solarMeter, // Compteur solaire → inféré par interface
|
|
battery, // Batterie → inférée ; charge pilotable (priorité + needs)
|
|
evCharger, // Borne de recharge
|
|
dhw, // Chauffe-eau (ECS)
|
|
heatPump, // Pompe à chaleur / SG-Ready
|
|
}
|
|
|
|
extension EmsRoleExt on EmsRole {
|
|
String get label => switch (this) {
|
|
EmsRole.evCharger => 'Borne de recharge',
|
|
EmsRole.dhw => 'Chauffe-eau',
|
|
EmsRole.heatPump => 'Pompe à chaleur',
|
|
EmsRole.battery => 'Batterie',
|
|
EmsRole.solarMeter => 'Compteur solaire',
|
|
EmsRole.gridMeter => 'Compteur réseau',
|
|
};
|
|
|
|
String get icon => switch (this) {
|
|
EmsRole.evCharger => '🚗',
|
|
EmsRole.dhw => '🌡️',
|
|
EmsRole.heatPump => '♨️',
|
|
EmsRole.battery => '🔋',
|
|
EmsRole.solarMeter => '☀️',
|
|
EmsRole.gridMeter => '⚡',
|
|
};
|
|
|
|
/// Interfaces nymea compatibles avec ce rôle.
|
|
List<String> get compatibleInterfaces => switch (this) {
|
|
EmsRole.evCharger => ['evcharger'],
|
|
EmsRole.dhw => ['simpleheatpump', 'relay', 'smartplug', 'powerswitch'],
|
|
EmsRole.heatPump => ['sgready', 'sgrelay', 'heatpump'],
|
|
EmsRole.battery => ['battery', 'energystorage', 'batterymonitor'],
|
|
EmsRole.solarMeter => ['solarinverter', 'energymeter', 'meter', 'inverter', 'smartmeter'],
|
|
EmsRole.gridMeter => ['energymeter', 'smartmeter', 'meter'],
|
|
};
|
|
|
|
/// Label d'interface affiché dans la liste de sélection des things.
|
|
String interfaceLabelFor(String iface) => switch (iface.toLowerCase()) {
|
|
'sgready' => 'SG-Ready',
|
|
'sgrelay' => 'SG-Ready',
|
|
'heatpump' => 'Heat Pump',
|
|
'simpleheatpump' => 'SimpleHeatpump',
|
|
'solarinverter' => 'Inverter',
|
|
'energymeter' => 'Meter',
|
|
'meter' => 'Meter',
|
|
'inverter' => 'Inverter',
|
|
'smartmeter' => 'Smart Meter',
|
|
'evcharger' => 'EV Charger',
|
|
'battery' => 'Battery',
|
|
'energystorage' => 'Energy Storage',
|
|
_ => iface,
|
|
};
|
|
}
|
|
|
|
/// Classification d'un rôle — détermine sa zone et son comportement (contrat).
|
|
extension EmsRoleClassify on EmsRole {
|
|
/// Rôle inféré par interface (solaire, batterie). Apparaît **direct** en zone
|
|
/// définitive avec un badge « détecté » — jamais en « À configurer », jamais
|
|
/// bloquant pour la complétude.
|
|
bool get isInferred => this == EmsRole.solarMeter || this == EmsRole.battery;
|
|
|
|
/// Rôle « pické » par l'installateur (réseau, EV, ECS, PAC). Compté quand
|
|
/// assigné **ou** acté « Pas d'appareil ».
|
|
bool get isPicked => !isInferred;
|
|
|
|
/// Compteur → zone Compteurs, carte non draggable.
|
|
bool get isMeter => this == EmsRole.gridMeter || this == EmsRole.solarMeter;
|
|
|
|
/// Charge pilotable → zone Charges pilotables, priorisable par drag.
|
|
bool get isControllableLoad =>
|
|
this == EmsRole.battery ||
|
|
this == EmsRole.evCharger ||
|
|
this == EmsRole.dhw ||
|
|
this == EmsRole.heatPump;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// État tri-état d'un rôle (contrat : present / absent / non-configuré)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// État d'un rôle. **Jamais `thingId:null`** : l'absence d'appareil est un état
|
|
/// explicite ([RoleAbsent]), distinct du « pas encore configuré »
|
|
/// ([RoleUnconfigured]).
|
|
sealed class RoleState {
|
|
const RoleState();
|
|
}
|
|
|
|
/// Rôle pas encore configuré (clé absente, au sens du contrat).
|
|
class RoleUnconfigured extends RoleState {
|
|
const RoleUnconfigured();
|
|
}
|
|
|
|
/// Rôle assigné à un thing concret.
|
|
class RolePresent extends RoleState {
|
|
final String thingId;
|
|
|
|
/// `true` si le rôle a été **inféré** par interface (solaire / batterie).
|
|
final bool detected;
|
|
|
|
const RolePresent(this.thingId, {this.detected = false});
|
|
}
|
|
|
|
/// Rôle acté « Pas d'appareil » (`{status: "absent"}`).
|
|
class RoleAbsent extends RoleState {
|
|
const RoleAbsent();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Déclaration de charge pilotable (LoadDescriptor §3 — édité écran 2)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
enum LoadMode { fixed, dynamic }
|
|
|
|
/// Besoins d'une charge pilotable (`needs` du LoadDescriptor).
|
|
class LoadNeeds {
|
|
final String? dailyDeadline; // ex. "06:00"
|
|
final int? minEnergyWhPerDay; // ex. 4000
|
|
|
|
const LoadNeeds({this.dailyDeadline, this.minEnergyWhPerDay});
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
if (dailyDeadline != null) 'dailyDeadline': dailyDeadline,
|
|
if (minEnergyWhPerDay != null) 'minEnergyWhPerDay': minEnergyWhPerDay,
|
|
};
|
|
}
|
|
|
|
/// Déclaration d'une charge pilotable, éditée dans l'**écran 2**.
|
|
///
|
|
/// Sur la carte de liste (écran 1), seul [summary] est montré — **pas d'éditeur
|
|
/// de paliers** (décision contrat : les paliers sont une déclaration, pas une
|
|
/// propriété matérielle).
|
|
class LoadConfig {
|
|
final LoadMode mode;
|
|
|
|
/// Paliers atteignables en W — REQUIS si [LoadMode.fixed], trié, contient `0`.
|
|
final List<int> powerLevels;
|
|
|
|
/// Plafond physique en W — REQUIS si [LoadMode.dynamic] ; plafond aussi en fixed.
|
|
final int? maxPowerW;
|
|
|
|
final LoadNeeds? needs;
|
|
|
|
const LoadConfig({
|
|
required this.mode,
|
|
this.powerLevels = const [],
|
|
this.maxPowerW,
|
|
this.needs,
|
|
});
|
|
|
|
/// Plafond effectif : `maxPowerW` sinon dernier palier déclaré.
|
|
int? get effectiveMaxW =>
|
|
maxPowerW ?? (powerLevels.isNotEmpty ? powerLevels.last : null);
|
|
|
|
/// Résumé affiché sur la carte de liste, ex. « 5 paliers · max 2400 W ».
|
|
String get summary {
|
|
if (mode == LoadMode.dynamic) {
|
|
return maxPowerW != null ? 'Dynamique · max $maxPowerW W' : 'Dynamique';
|
|
}
|
|
final n = powerLevels.length;
|
|
final maxW = effectiveMaxW;
|
|
return maxW != null ? '$n paliers · max $maxW W' : '$n paliers';
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// État global de l'écran Rôles & appareils
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Trois états d'écran (contrat) : vierge / en cours / complet.
|
|
enum SetupScreenState { blank, inProgress, complete }
|
|
|
|
/// Source de vérité **unique** des rôles EMS et de leur configuration.
|
|
///
|
|
/// Modèle tri-état conforme à `INTERFACE_etmvariableload.md`. La persistance
|
|
/// réelle (`Energy.SetRootMeter`, `NymeaEnergy.Set/GetLoadConfig`) est branchée
|
|
/// à l'**Étape 2** ; ce provider ne gère que l'état en mémoire et son routage.
|
|
class EnergySetupProvider extends ChangeNotifier {
|
|
/// Ordre canonique d'insertion des charges pilotables (contrat :
|
|
/// EV → ECS → PAC → Batterie). L'installateur réordonne ensuite par drag.
|
|
static const List<EmsRole> _canonicalLoadOrder = [
|
|
EmsRole.evCharger,
|
|
EmsRole.dhw,
|
|
EmsRole.heatPump,
|
|
EmsRole.battery,
|
|
];
|
|
|
|
final Map<EmsRole, RoleState> _states = {
|
|
for (final r in EmsRole.values) r: const RoleUnconfigured(),
|
|
};
|
|
|
|
final Map<EmsRole, LoadConfig> _loadConfigs = {};
|
|
final Map<EmsRole, bool> _enabled = {};
|
|
|
|
/// Charges pilotables présentes, dans l'ordre de priorité (index 0 = rang 1).
|
|
final List<EmsRole> _priorityOrder = [];
|
|
|
|
/// Service de transport (Étape 2) — attaché par l'écran une fois connecté.
|
|
NymeaService? _service;
|
|
|
|
/// Dernier rootMeter poussé — évite de renvoyer `SetRootMeter` (RPC admin)
|
|
/// à chaque mutation.
|
|
String? _lastRootMeterId;
|
|
|
|
// ── Lecture d'état ─────────────────────────────────────────────────────────
|
|
|
|
RoleState stateOf(EmsRole role) => _states[role]!;
|
|
LoadConfig? loadConfigOf(EmsRole role) => _loadConfigs[role];
|
|
bool isEnabled(EmsRole role) => _enabled[role] ?? true;
|
|
|
|
bool isPresent(EmsRole role) => _states[role] is RolePresent;
|
|
bool isAbsent(EmsRole role) => _states[role] is RoleAbsent;
|
|
bool isUnconfigured(EmsRole role) => _states[role] is RoleUnconfigured;
|
|
|
|
/// `true` si le rôle a été inféré par interface.
|
|
bool isDetected(EmsRole role) {
|
|
final s = _states[role];
|
|
return s is RolePresent && s.detected;
|
|
}
|
|
|
|
/// Thing assigné au rôle, ou `null` si non présent.
|
|
String? thingIdOf(EmsRole role) {
|
|
final s = _states[role];
|
|
return s is RolePresent ? s.thingId : null;
|
|
}
|
|
|
|
/// Rang de priorité (1 = servi en premier), ou `null` si non priorisé.
|
|
int? priorityOf(EmsRole role) {
|
|
final i = _priorityOrder.indexOf(role);
|
|
return i < 0 ? null : i + 1;
|
|
}
|
|
|
|
// ── Zones d'affichage ──────────────────────────────────────────────────────
|
|
|
|
/// Compteurs présents (réseau, solaire détecté). Non draggables.
|
|
List<EmsRole> get meterRoles =>
|
|
EmsRole.values.where((r) => r.isMeter && isPresent(r)).toList();
|
|
|
|
/// Charges pilotables présentes, **dans l'ordre de priorité**.
|
|
List<EmsRole> get controllableRoles => List.unmodifiable(_priorityOrder);
|
|
|
|
/// Rôles pickés encore non configurés → zone « À configurer ».
|
|
/// Les rôles inférés n'y figurent jamais.
|
|
List<EmsRole> get toConfigureRoles =>
|
|
EmsRole.values.where((r) => r.isPicked && isUnconfigured(r)).toList();
|
|
|
|
/// Rôles actés « Pas d'appareil ».
|
|
List<EmsRole> get absentRoles =>
|
|
EmsRole.values.where((r) => isAbsent(r)).toList();
|
|
|
|
// ── Compteur X/6 & complétude ──────────────────────────────────────────────
|
|
|
|
int get totalRoles => EmsRole.values.length;
|
|
|
|
/// Rôles configurés : inférés comptés d'office ; pickés comptés quand assignés
|
|
/// **ou** actés absents.
|
|
int get configuredCount {
|
|
var n = 0;
|
|
for (final r in EmsRole.values) {
|
|
if (r.isInferred) {
|
|
n++; // configurés d'office
|
|
} else if (_states[r] is RolePresent || _states[r] is RoleAbsent) {
|
|
n++;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/// « Complet » = aucun rôle pické en non-configuré (les inférés ne bloquent
|
|
/// jamais).
|
|
bool get isComplete => EmsRole.values
|
|
.where((r) => r.isPicked)
|
|
.every((r) => _states[r] is! RoleUnconfigured);
|
|
|
|
SetupScreenState get screenState {
|
|
final anyPresent = _states.values.any((s) => s is RolePresent);
|
|
if (!anyPresent) return SetupScreenState.blank;
|
|
return isComplete ? SetupScreenState.complete : SetupScreenState.inProgress;
|
|
}
|
|
|
|
// ── Mutations ──────────────────────────────────────────────────────────────
|
|
|
|
/// Assigne un thing à un rôle. Une charge pilotable rejoint la liste de
|
|
/// priorité à sa place canonique.
|
|
void assignThing(EmsRole role, String thingId) {
|
|
_states[role] = RolePresent(thingId);
|
|
if (role.isControllableLoad) _ensureInOrder(role);
|
|
_commit();
|
|
}
|
|
|
|
/// Acte « Pas d'appareil » pour un rôle.
|
|
void markAbsent(EmsRole role) {
|
|
_states[role] = const RoleAbsent();
|
|
_priorityOrder.remove(role);
|
|
_commit();
|
|
}
|
|
|
|
/// Remet un rôle en « À configurer ».
|
|
void unconfigure(EmsRole role) {
|
|
_states[role] = const RoleUnconfigured();
|
|
_priorityOrder.remove(role);
|
|
_commit();
|
|
}
|
|
|
|
/// Active / exclut une charge de l'arbitrage (`enabled` du LoadDescriptor).
|
|
void setEnabled(EmsRole role, bool enabled) {
|
|
_enabled[role] = enabled;
|
|
_commit();
|
|
}
|
|
|
|
/// Enregistre la déclaration éditée dans l'écran 2.
|
|
void setLoadConfig(EmsRole role, LoadConfig config) {
|
|
_loadConfigs[role] = config;
|
|
_commit();
|
|
}
|
|
|
|
/// Réordonne les charges pilotables → réécrit la priorité (rang 1 = premier
|
|
/// servi). Sémantique `ReorderableListView` (newIndex décalé vers le haut).
|
|
void reorderControllableLoads(int oldIndex, int newIndex) {
|
|
if (oldIndex < 0 || oldIndex >= _priorityOrder.length) return;
|
|
if (newIndex > oldIndex) newIndex--;
|
|
final role = _priorityOrder.removeAt(oldIndex);
|
|
_priorityOrder.insert(newIndex.clamp(0, _priorityOrder.length), role);
|
|
_commit();
|
|
}
|
|
|
|
// ── Persistance côté plugin (Étape 2) ──────────────────────────────────────
|
|
|
|
/// Attache le service de transport. Appelé par l'écran une fois connecté.
|
|
/// Tente une lecture initiale (stub vide — ne bloque pas le démarrage).
|
|
void attach(NymeaService service) {
|
|
if (identical(_service, service)) return;
|
|
_service = service;
|
|
service.getLoadConfig();
|
|
}
|
|
|
|
/// `etmvariableload` = charge à **puissance pilotable** (rév. 2). La **PAC**
|
|
/// en est exclue (modèle à états SG-Ready, §8) ; la **batterie** aussi (kind
|
|
/// `Constraint`, déféré 3f). Seuls EV & ECS émettent un `LoadDescriptor`.
|
|
bool isVariableLoad(EmsRole role) =>
|
|
role == EmsRole.evCharger || role == EmsRole.dhw;
|
|
|
|
/// Construit le `LoadDescriptor` (§4) d'une charge etmvariableload présente,
|
|
/// ou `null` sinon. Les champs déclarés (`mode`/`powerLevels`/`maxPowerW`/
|
|
/// `needs`) ne sont posés que si la déclaration (écran 2) existe — sinon seul
|
|
/// l'ossature priorité/activation part (déclaration encore à faire).
|
|
Map<String, dynamic>? buildLoadDescriptor(EmsRole role) {
|
|
if (!isVariableLoad(role)) return null;
|
|
final thingId = thingIdOf(role);
|
|
if (thingId == null) return null;
|
|
|
|
final d = <String, dynamic>{
|
|
'id': thingId,
|
|
'label': role.label,
|
|
'adapter': 'etmvariableload',
|
|
'priority': priorityOf(role) ?? 0,
|
|
'enabled': isEnabled(role),
|
|
};
|
|
|
|
final cfg = _loadConfigs[role];
|
|
if (cfg != null) {
|
|
d['mode'] = cfg.mode == LoadMode.fixed ? 'fixed' : 'dynamic';
|
|
if (cfg.mode == LoadMode.fixed) d['powerLevels'] = cfg.powerLevels;
|
|
if (cfg.maxPowerW != null) d['maxPowerW'] = cfg.maxPowerW;
|
|
if (cfg.needs != null) d['needs'] = cfg.needs!.toJson();
|
|
}
|
|
return d;
|
|
}
|
|
|
|
/// Tous les `LoadDescriptor` à émettre, dans l'ordre de priorité.
|
|
List<Map<String, dynamic>> buildLoadDescriptors() => controllableRoles
|
|
.map(buildLoadDescriptor)
|
|
.whereType<Map<String, dynamic>>()
|
|
.toList();
|
|
|
|
/// Pousse l'état courant vers le plugin :
|
|
/// • gridMeter → `Energy.SetRootMeter` (**réel**, seulement si changé) ;
|
|
/// • EV / ECS → `SetLoadConfig` (**stub** : logge le JSON) ;
|
|
/// • PAC / batterie → chemins distincts (rév. 2), tracés explicitement.
|
|
void persist() {
|
|
final service = _service;
|
|
if (service == null) return;
|
|
|
|
final gridId = thingIdOf(EmsRole.gridMeter);
|
|
if (gridId != null && gridId != _lastRootMeterId) {
|
|
_lastRootMeterId = gridId;
|
|
service.setRootMeter(gridId);
|
|
}
|
|
|
|
service.setLoadConfig(buildLoadDescriptors());
|
|
|
|
for (final role in controllableRoles) {
|
|
if (role == EmsRole.heatPump) {
|
|
service.logInfo('[LoadConfig STUB] PAC "${role.label}" → action état '
|
|
'SG-Ready (hors LoadDescriptor, chemin §8)');
|
|
} else if (role == EmsRole.battery) {
|
|
service.logInfo('[LoadConfig STUB] Batterie "${role.label}" → kind '
|
|
'Constraint (déféré 3f), non émis');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Notifie l'UI **et** pousse vers le plugin.
|
|
void _commit() {
|
|
notifyListeners();
|
|
persist();
|
|
}
|
|
|
|
/// Réinitialise l'état au **switch d'installation**. Reset mémoire **sans
|
|
/// `persist()`** : on ne pousse jamais une config vide vers la nouvelle box
|
|
/// (sinon on écraserait sa vraie config au connect). Garde la ref `_service`.
|
|
void clearForSwitch() {
|
|
for (final r in EmsRole.values) {
|
|
_states[r] = const RoleUnconfigured();
|
|
}
|
|
_loadConfigs.clear();
|
|
_enabled.clear();
|
|
_priorityOrder.clear();
|
|
_lastRootMeterId = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
// ── Inférence solaire / batterie ───────────────────────────────────────────
|
|
|
|
/// Infère les rôles solaire & batterie depuis les interfaces des things : ils
|
|
/// passent **direct** en zone définitive (badge « détecté »). N'écrase jamais
|
|
/// un choix explicite (assignation manuelle ou « Pas d'appareil »).
|
|
void syncFromThings(NymeaService service) {
|
|
var changed = false;
|
|
for (final role in const [EmsRole.solarMeter, EmsRole.battery]) {
|
|
if (_states[role] is! RoleUnconfigured) continue; // respecte l'explicite
|
|
final matches = compatibleThings(role, service);
|
|
if (matches.isNotEmpty) {
|
|
_states[role] = RolePresent(matches.first.id, detected: true);
|
|
if (role.isControllableLoad) _ensureInOrder(role);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) notifyListeners();
|
|
}
|
|
|
|
// ── Filtrage des things compatibles avec un rôle ───────────────────────────
|
|
|
|
List<NymeaThing> compatibleThings(EmsRole role, NymeaService service) {
|
|
return service.things.where((thing) {
|
|
try {
|
|
final cls =
|
|
service.thingClasses.firstWhere((c) => c.id == thing.thingClassId);
|
|
return cls.interfaces
|
|
.any((i) => role.compatibleInterfaces.contains(i.toLowerCase()));
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}).toList();
|
|
}
|
|
|
|
// ── Interne ────────────────────────────────────────────────────────────────
|
|
|
|
/// Insère une charge pilotable à sa place canonique si absente de l'ordre.
|
|
void _ensureInOrder(EmsRole role) {
|
|
if (_priorityOrder.contains(role)) return;
|
|
final canonIdx = _canonicalLoadOrder.indexOf(role);
|
|
var insertAt = _priorityOrder.length;
|
|
for (var i = 0; i < _priorityOrder.length; i++) {
|
|
if (_canonicalLoadOrder.indexOf(_priorityOrder[i]) > canonIdx) {
|
|
insertAt = i;
|
|
break;
|
|
}
|
|
}
|
|
_priorityOrder.insert(insertAt, role);
|
|
}
|
|
}
|