| | 1 | | using LGDXRobotCloud.API.Exceptions; |
| | 2 | | using LGDXRobotCloud.API.Repositories; |
| | 3 | | using LGDXRobotCloud.API.Services.Administration; |
| | 4 | | using LGDXRobotCloud.API.Services.Navigation; |
| | 5 | | using LGDXRobotCloud.Data.DbContexts; |
| | 6 | | using LGDXRobotCloud.Data.Entities; |
| | 7 | | using LGDXRobotCloud.Data.Models.Business.Administration; |
| | 8 | | using LGDXRobotCloud.Data.Models.Business.Automation; |
| | 9 | | using LGDXRobotCloud.Data.Models.Business.Navigation; |
| | 10 | | using LGDXRobotCloud.Utilities.Enums; |
| | 11 | | using LGDXRobotCloud.Utilities.Helpers; |
| | 12 | | using Microsoft.EntityFrameworkCore; |
| | 13 | | using Microsoft.Extensions.Caching.Memory; |
| | 14 | |
|
| | 15 | | namespace LGDXRobotCloud.API.Services.Automation; |
| | 16 | |
|
| | 17 | | public interface IAutoTaskService |
| | 18 | | { |
| | 19 | | Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? name, AutoTas |
| | 20 | | Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId); |
| | 21 | | Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel); |
| | 22 | | Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel); |
| | 23 | | Task<bool> DeleteAutoTaskAsync(int autoTaskId); |
| | 24 | |
|
| | 25 | | Task AbortAutoTaskAsync(int autoTaskId); |
| | 26 | | Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token); |
| | 27 | |
|
| | 28 | | Task<AutoTaskStatisticsBusinessModel> GetAutoTaskStatisticsAsync(int realmId); |
| | 29 | | Task<AutoTaskListBusinessModel?> GetRobotCurrentTaskAsync(Guid robotId); |
| | 30 | | } |
| | 31 | |
|
| 31 | 32 | | public class AutoTaskService( |
| 31 | 33 | | IActivityLogService activityLogService, |
| 31 | 34 | | IAutoTaskRepository autoTaskRepository, |
| 31 | 35 | | IAutoTaskSchedulerService autoTaskSchedulerService, |
| 31 | 36 | | IMemoryCache memoryCache, |
| 31 | 37 | | IOnlineRobotsService onlineRobotsService, |
| 31 | 38 | | LgdxContext context |
| 31 | 39 | | ) : IAutoTaskService |
| | 40 | | { |
| 31 | 41 | | private readonly IActivityLogService _activityLogService = activityLogService ?? throw new ArgumentNullException(nameo |
| 31 | 42 | | private readonly IAutoTaskRepository _autoTaskRepository = autoTaskRepository ?? throw new ArgumentNullException(nameo |
| 31 | 43 | | private readonly IAutoTaskSchedulerService _autoTaskSchedulerService = autoTaskSchedulerService ?? throw new ArgumentN |
| 31 | 44 | | private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); |
| 31 | 45 | | private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na |
| 31 | 46 | | private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context)); |
| | 47 | |
|
| | 48 | | public async Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? |
| 9 | 49 | | { |
| 9 | 50 | | var query = _context.AutoTasks as IQueryable<AutoTask>; |
| 9 | 51 | | if (!string.IsNullOrWhiteSpace(name)) |
| 3 | 52 | | { |
| 3 | 53 | | name = name.Trim(); |
| 3 | 54 | | query = query.Where(t => t.Name != null && t.Name.ToLower().Contains(name.ToLower())); |
| 3 | 55 | | } |
| 9 | 56 | | if (realmId != null) |
| 9 | 57 | | { |
| 9 | 58 | | query = query.Where(t => t.RealmId == realmId); |
| 9 | 59 | | } |
| 9 | 60 | | switch (autoTaskCatrgory) |
| | 61 | | { |
| | 62 | | case AutoTaskCatrgory.Template: |
| 1 | 63 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Template); |
| 1 | 64 | | break; |
| | 65 | | case AutoTaskCatrgory.Waiting: |
| 1 | 66 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Waiting); |
| 1 | 67 | | break; |
| | 68 | | case AutoTaskCatrgory.Completed: |
| 1 | 69 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Completed); |
| 1 | 70 | | break; |
| | 71 | | case AutoTaskCatrgory.Aborted: |
| 1 | 72 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Aborted); |
| 1 | 73 | | break; |
| | 74 | | case AutoTaskCatrgory.Running: |
| 1 | 75 | | query = query.Where(t => t.CurrentProgressId > (int)ProgressState.Aborted); |
| 1 | 76 | | break; |
| | 77 | | } |
| 9 | 78 | | var itemCount = await query.CountAsync(); |
| 9 | 79 | | var PaginationHelper = new PaginationHelper(itemCount, pageNumber, pageSize); |
| 9 | 80 | | var autoTasks = await query.AsNoTracking() |
| 9 | 81 | | .OrderByDescending(t => t.Priority) |
| 9 | 82 | | .ThenBy(t => t.Id) |
| 9 | 83 | | .Skip(pageSize * (pageNumber - 1)) |
| 9 | 84 | | .Take(pageSize) |
| 9 | 85 | | .Select(t => new AutoTaskListBusinessModel |
| 9 | 86 | | { |
| 9 | 87 | | Id = t.Id, |
| 9 | 88 | | Name = t.Name, |
| 9 | 89 | | Priority = t.Priority, |
| 9 | 90 | | FlowId = t.Flow!.Id, |
| 9 | 91 | | FlowName = t.Flow.Name, |
| 9 | 92 | | RealmId = t.Realm.Id, |
| 9 | 93 | | RealmName = t.Realm.Name, |
| 9 | 94 | | AssignedRobotId = t.AssignedRobotId, |
| 9 | 95 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 9 | 96 | | CurrentProgressId = t.CurrentProgressId, |
| 9 | 97 | | CurrentProgressName = t.CurrentProgress.Name, |
| 9 | 98 | | }) |
| 9 | 99 | | .AsSplitQuery() |
| 9 | 100 | | .ToListAsync(); |
| 9 | 101 | | return (autoTasks, PaginationHelper); |
| 9 | 102 | | } |
| | 103 | |
|
| | 104 | | public async Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId) |
| 2 | 105 | | { |
| 2 | 106 | | return await _context.AutoTasks.AsNoTracking() |
| 2 | 107 | | .Where(t => t.Id == autoTaskId) |
| 2 | 108 | | .Include(t => t.AutoTaskDetails |
| 2 | 109 | | .OrderBy(td => td.Order)) |
| 2 | 110 | | .ThenInclude(td => td.Waypoint) |
| 2 | 111 | | .Include(t => t.AutoTaskJourneys) |
| 2 | 112 | | .ThenInclude(tj => tj.CurrentProgress) |
| 2 | 113 | | .Include(t => t.AssignedRobot) |
| 2 | 114 | | .Include(t => t.CurrentProgress) |
| 2 | 115 | | .Include(t => t.Flow) |
| 2 | 116 | | .Include(t => t.Realm) |
| 2 | 117 | | .AsSplitQuery() |
| 2 | 118 | | .Select(t => new AutoTaskBusinessModel |
| 2 | 119 | | { |
| 2 | 120 | | Id = t.Id, |
| 2 | 121 | | Name = t.Name, |
| 2 | 122 | | Priority = t.Priority, |
| 2 | 123 | | FlowId = t.Flow!.Id, |
| 2 | 124 | | FlowName = t.Flow.Name, |
| 2 | 125 | | RealmId = t.Realm.Id, |
| 2 | 126 | | RealmName = t.Realm.Name, |
| 2 | 127 | | AssignedRobotId = t.AssignedRobotId, |
| 2 | 128 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 2 | 129 | | CurrentProgressId = t.CurrentProgressId, |
| 2 | 130 | | CurrentProgressName = t.CurrentProgress.Name, |
| 2 | 131 | | AutoTaskJourneys = t.AutoTaskJourneys.Select(tj => new AutoTaskJourneyBusinessModel |
| 2 | 132 | | { |
| 2 | 133 | | Id = tj.Id, |
| 2 | 134 | | CurrentProcessId = tj.CurrentProgressId, |
| 2 | 135 | | CurrentProcessName = tj.CurrentProgress == null ? null : tj.CurrentProgress.Name, |
| 2 | 136 | | CreatedAt = tj.CreatedAt, |
| 2 | 137 | | }) |
| 2 | 138 | | .OrderBy(tj => tj.Id) |
| 2 | 139 | | .ToList(), |
| 2 | 140 | | AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel |
| 2 | 141 | | { |
| 2 | 142 | | Id = td.Id, |
| 2 | 143 | | Order = td.Order, |
| 2 | 144 | | CustomX = td.CustomX, |
| 2 | 145 | | CustomY = td.CustomY, |
| 2 | 146 | | CustomRotation = td.CustomRotation, |
| 2 | 147 | | Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel |
| 2 | 148 | | { |
| 2 | 149 | | Id = td.Waypoint.Id, |
| 2 | 150 | | Name = td.Waypoint.Name, |
| 2 | 151 | | RealmId = t.Realm.Id, |
| 2 | 152 | | RealmName = t.Realm.Name, |
| 2 | 153 | | X = td.Waypoint.X, |
| 2 | 154 | | Y = td.Waypoint.Y, |
| 2 | 155 | | Rotation = td.Waypoint.Rotation, |
| 2 | 156 | | IsParking = td.Waypoint.IsParking, |
| 2 | 157 | | HasCharger = td.Waypoint.HasCharger, |
| 2 | 158 | | IsReserved = td.Waypoint.IsReserved, |
| 2 | 159 | | }, |
| 2 | 160 | | }) |
| 2 | 161 | | .OrderBy(td => td.Order) |
| 2 | 162 | | .ToList() |
| 2 | 163 | | }) |
| 2 | 164 | | .FirstOrDefaultAsync() |
| 2 | 165 | | ?? throw new LgdxNotFound404Exception(); |
| 1 | 166 | | } |
| | 167 | |
|
| | 168 | | private async Task ValidateAutoTask(HashSet<int> waypointIds, int flowId, int realmId, Guid? robotId) |
| 7 | 169 | | { |
| | 170 | | // Validate the Waypoint IDs |
| 7 | 171 | | var waypointDict = await _context.Waypoints.AsNoTracking() |
| 7 | 172 | | .Where(w => waypointIds.Contains(w.Id)) |
| 7 | 173 | | .Where(w => w.RealmId == realmId) |
| 11 | 174 | | .ToDictionaryAsync(w => w.Id, w => w); |
| 26 | 175 | | foreach (var waypointId in waypointIds) |
| 3 | 176 | | { |
| 3 | 177 | | if (!waypointDict.ContainsKey(waypointId)) |
| 1 | 178 | | { |
| 1 | 179 | | throw new LgdxValidation400Expection(nameof(Waypoint), $"The Waypoint ID {waypointId} is invalid."); |
| | 180 | | } |
| 2 | 181 | | } |
| | 182 | | // Validate the Flow ID |
| 6 | 183 | | var flow = await _context.Flows.Where(f => f.Id == flowId).AnyAsync(); |
| 6 | 184 | | if (flow == false) |
| 1 | 185 | | { |
| 1 | 186 | | throw new LgdxValidation400Expection(nameof(Flow), $"The Flow ID {flowId} is invalid."); |
| | 187 | | } |
| | 188 | | // Validate the Realm ID |
| 5 | 189 | | var realm = await _context.Realms.Where(r => r.Id == realmId).AnyAsync(); |
| 5 | 190 | | if (realm == false) |
| 1 | 191 | | { |
| 1 | 192 | | throw new LgdxValidation400Expection(nameof(Realm), $"The Realm ID {realmId} is invalid."); |
| | 193 | | } |
| | 194 | | // Validate the Assigned Robot ID |
| 4 | 195 | | if (robotId != null) |
| 4 | 196 | | { |
| 4 | 197 | | var robot = await _context |
| 4 | 198 | | .Robots |
| 4 | 199 | | .Where(r => r.Id == robotId) |
| 4 | 200 | | .Where(r => r.RealmId == realmId) |
| 4 | 201 | | .AnyAsync(); |
| 4 | 202 | | if (robot == false) |
| 1 | 203 | | { |
| 1 | 204 | | throw new LgdxValidation400Expection(nameof(Robot), $"Robot ID: {robotId} is invalid."); |
| | 205 | | } |
| 3 | 206 | | } |
| 3 | 207 | | } |
| | 208 | |
|
| | 209 | | public async Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel) |
| 6 | 210 | | { |
| | 211 | | // Waypoint is needed when Waypoins Traffic Control is enabled |
| 6 | 212 | | bool hasWaypointsTrafficControl = await _context.Realms.AsNoTracking() |
| 6 | 213 | | .Where(r => r.Id == autoTaskCreateBusinessModel.RealmId) |
| 6 | 214 | | .Select(r => r.HasWaypointsTrafficControl) |
| 6 | 215 | | .FirstOrDefaultAsync(); |
| 6 | 216 | | if (hasWaypointsTrafficControl) |
| 0 | 217 | | { |
| 0 | 218 | | foreach (var detail in autoTaskCreateBusinessModel.AutoTaskDetails) |
| 0 | 219 | | { |
| 0 | 220 | | if (detail.WaypointId == null) |
| 0 | 221 | | { |
| 0 | 222 | | throw new LgdxValidation400Expection(nameof(detail.WaypointId), "Waypoint is required when Waypoints Traffic C |
| | 223 | | } |
| 0 | 224 | | } |
| 0 | 225 | | } |
| | 226 | |
|
| 6 | 227 | | HashSet<int> waypointIds = autoTaskCreateBusinessModel.AutoTaskDetails |
| 3 | 228 | | .Where(d => d.WaypointId != null) |
| 3 | 229 | | .Select(d => d.WaypointId!.Value) |
| 6 | 230 | | .ToHashSet(); |
| | 231 | |
|
| 6 | 232 | | await ValidateAutoTask(waypointIds, |
| 6 | 233 | | autoTaskCreateBusinessModel.FlowId, |
| 6 | 234 | | autoTaskCreateBusinessModel.RealmId, |
| 6 | 235 | | autoTaskCreateBusinessModel.AssignedRobotId); |
| | 236 | |
|
| 2 | 237 | | var autoTask = new AutoTask |
| 2 | 238 | | { |
| 2 | 239 | | Name = autoTaskCreateBusinessModel.Name, |
| 2 | 240 | | Priority = autoTaskCreateBusinessModel.Priority, |
| 2 | 241 | | FlowId = autoTaskCreateBusinessModel.FlowId, |
| 2 | 242 | | RealmId = autoTaskCreateBusinessModel.RealmId, |
| 2 | 243 | | AssignedRobotId = autoTaskCreateBusinessModel.AssignedRobotId, |
| 2 | 244 | | CurrentProgressId = autoTaskCreateBusinessModel.IsTemplate |
| 2 | 245 | | ? (int)ProgressState.Template |
| 2 | 246 | | : (int)ProgressState.Waiting, |
| 2 | 247 | | AutoTaskDetails = autoTaskCreateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail |
| 2 | 248 | | { |
| 2 | 249 | | Order = td.Order, |
| 2 | 250 | | CustomX = td.CustomX, |
| 2 | 251 | | CustomY = td.CustomY, |
| 2 | 252 | | CustomRotation = td.CustomRotation, |
| 2 | 253 | | WaypointId = td.WaypointId, |
| 2 | 254 | | }) |
| 0 | 255 | | .OrderBy(td => td.Order) |
| 2 | 256 | | .ToList(), |
| 2 | 257 | | }; |
| 2 | 258 | | await _context.AutoTasks.AddAsync(autoTask); |
| 2 | 259 | | await _context.SaveChangesAsync(); |
| | 260 | |
|
| 2 | 261 | | var autoTaskBusinessModel = await _context.AutoTasks.AsNoTracking() |
| 2 | 262 | | .Where(t => t.Id == autoTask.Id) |
| 2 | 263 | | .Select(t => new AutoTaskBusinessModel |
| 2 | 264 | | { |
| 2 | 265 | | Id = t.Id, |
| 2 | 266 | | Name = t.Name, |
| 2 | 267 | | Priority = t.Priority, |
| 2 | 268 | | FlowId = t.Flow!.Id, |
| 2 | 269 | | FlowName = t.Flow.Name, |
| 2 | 270 | | RealmId = t.Realm.Id, |
| 2 | 271 | | RealmName = t.Realm.Name, |
| 2 | 272 | | AssignedRobotId = t.AssignedRobotId, |
| 2 | 273 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 2 | 274 | | CurrentProgressId = t.CurrentProgressId, |
| 2 | 275 | | CurrentProgressName = t.CurrentProgress.Name, |
| 2 | 276 | | AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel |
| 2 | 277 | | { |
| 2 | 278 | | Id = td.Id, |
| 2 | 279 | | Order = td.Order, |
| 2 | 280 | | CustomX = td.CustomX, |
| 2 | 281 | | CustomY = td.CustomY, |
| 2 | 282 | | CustomRotation = td.CustomRotation, |
| 2 | 283 | | Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel |
| 2 | 284 | | { |
| 2 | 285 | | Id = td.Waypoint.Id, |
| 2 | 286 | | Name = td.Waypoint.Name, |
| 2 | 287 | | RealmId = t.Realm.Id, |
| 2 | 288 | | RealmName = t.Realm.Name, |
| 2 | 289 | | X = td.Waypoint.X, |
| 2 | 290 | | Y = td.Waypoint.Y, |
| 2 | 291 | | Rotation = td.Waypoint.Rotation, |
| 2 | 292 | | IsParking = td.Waypoint.IsParking, |
| 2 | 293 | | HasCharger = td.Waypoint.HasCharger, |
| 2 | 294 | | IsReserved = td.Waypoint.IsReserved, |
| 2 | 295 | | }, |
| 2 | 296 | | }) |
| 2 | 297 | | .OrderBy(td => td.Order) |
| 2 | 298 | | .ToList() |
| 2 | 299 | | }) |
| 2 | 300 | | .FirstOrDefaultAsync() |
| 2 | 301 | | ?? throw new LgdxNotFound404Exception(); |
| | 302 | |
|
| 2 | 303 | | if (autoTask.CurrentProgressId == (int)ProgressState.Waiting) |
| 1 | 304 | | { |
| 1 | 305 | | var autoTaskJourney = new AutoTaskJourney |
| 1 | 306 | | { |
| 1 | 307 | | AutoTaskId = autoTaskBusinessModel.Id, |
| 1 | 308 | | CurrentProgressId = autoTask.CurrentProgressId |
| 1 | 309 | | }; |
| 1 | 310 | | await _context.AutoTasksJourney.AddAsync(autoTaskJourney); |
| 1 | 311 | | await _context.SaveChangesAsync(); |
| 1 | 312 | | await _autoTaskRepository.AutoTaskHasUpdateAsync(autoTask.RealmId, autoTaskBusinessModel.ToContract()); |
| 1 | 313 | | await _autoTaskSchedulerService.RunSchedulerNewAutoTaskAsync(autoTask.RealmId, autoTask.AssignedRobotId); |
| 1 | 314 | | } |
| | 315 | |
|
| 2 | 316 | | await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel |
| 2 | 317 | | { |
| 2 | 318 | | EntityName = nameof(AutoTask), |
| 2 | 319 | | EntityId = autoTaskBusinessModel.Id.ToString(), |
| 2 | 320 | | Action = ActivityAction.Create, |
| 2 | 321 | | }); |
| | 322 | |
|
| 2 | 323 | | return autoTaskBusinessModel; |
| 2 | 324 | | } |
| | 325 | |
|
| | 326 | | public async Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel) |
| 4 | 327 | | { |
| 4 | 328 | | var task = await _context.AutoTasks |
| 4 | 329 | | .Where(t => t.Id == autoTaskId) |
| 4 | 330 | | .Include(t => t.AutoTaskDetails |
| 4 | 331 | | .OrderBy(td => td.Order)) |
| 4 | 332 | | .FirstOrDefaultAsync() |
| 4 | 333 | | ?? throw new LgdxNotFound404Exception(); |
| | 334 | |
|
| 3 | 335 | | if (task.CurrentProgressId != (int)ProgressState.Template) |
| 1 | 336 | | { |
| 1 | 337 | | throw new LgdxValidation400Expection("AutoTaskId", "Only AutoTask Templates are editable."); |
| | 338 | | } |
| | 339 | | // Ensure the input task does not include Detail ID from other task |
| 6 | 340 | | HashSet<int> dbDetailIds = task.AutoTaskDetails.Select(d => d.Id).ToHashSet(); |
| 9 | 341 | | foreach (var bmDetailId in autoTaskUpdateBusinessModel.AutoTaskDetails.Where(d => d.Id != null).Select(d => d.Id)) |
| 1 | 342 | | { |
| 1 | 343 | | if (bmDetailId != null && !dbDetailIds.Contains((int)bmDetailId)) |
| 1 | 344 | | { |
| 1 | 345 | | throw new LgdxValidation400Expection("AutoTaskDetailId", $"The Task Detail ID {(int)bmDetailId} is belongs to ot |
| | 346 | | } |
| 0 | 347 | | } |
| | 348 | | // Waypoint is needed when Waypoins Traffic Control is enabled |
| 1 | 349 | | bool hasWaypointsTrafficControl = await _context.Realms.AsNoTracking() |
| 1 | 350 | | .Where(r => r.Id == task.RealmId) |
| 1 | 351 | | .Select(r => r.HasWaypointsTrafficControl) |
| 1 | 352 | | .FirstOrDefaultAsync(); |
| 1 | 353 | | if (hasWaypointsTrafficControl) |
| 0 | 354 | | { |
| 0 | 355 | | foreach (var detail in autoTaskUpdateBusinessModel.AutoTaskDetails) |
| 0 | 356 | | { |
| 0 | 357 | | if (detail.WaypointId == null) |
| 0 | 358 | | { |
| 0 | 359 | | throw new LgdxValidation400Expection(nameof(detail.WaypointId), "Waypoint is required when Waypoints Traffic C |
| | 360 | | } |
| 0 | 361 | | } |
| 0 | 362 | | } |
| 1 | 363 | | HashSet<int> waypointIds = autoTaskUpdateBusinessModel.AutoTaskDetails |
| 0 | 364 | | .Where(d => d.WaypointId != null) |
| 0 | 365 | | .Select(d => d.WaypointId!.Value) |
| 1 | 366 | | .ToHashSet(); |
| 1 | 367 | | await ValidateAutoTask(waypointIds, |
| 1 | 368 | | autoTaskUpdateBusinessModel.FlowId, |
| 1 | 369 | | task.RealmId, |
| 1 | 370 | | autoTaskUpdateBusinessModel.AssignedRobotId); |
| | 371 | |
|
| 1 | 372 | | task.Name = autoTaskUpdateBusinessModel.Name; |
| 1 | 373 | | task.Priority = autoTaskUpdateBusinessModel.Priority; |
| 1 | 374 | | task.FlowId = autoTaskUpdateBusinessModel.FlowId; |
| 1 | 375 | | task.AssignedRobotId = autoTaskUpdateBusinessModel.AssignedRobotId; |
| 1 | 376 | | task.AutoTaskDetails = autoTaskUpdateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail |
| 1 | 377 | | { |
| 1 | 378 | | Id = (int)td.Id!, |
| 1 | 379 | | Order = td.Order, |
| 1 | 380 | | CustomX = td.CustomX, |
| 1 | 381 | | CustomY = td.CustomY, |
| 1 | 382 | | CustomRotation = td.CustomRotation, |
| 1 | 383 | | WaypointId = td.WaypointId, |
| 1 | 384 | | }).ToList(); |
| 1 | 385 | | await _context.SaveChangesAsync(); |
| | 386 | |
|
| 1 | 387 | | await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel |
| 1 | 388 | | { |
| 1 | 389 | | EntityName = nameof(AutoTask), |
| 1 | 390 | | EntityId = autoTaskId.ToString(), |
| 1 | 391 | | Action = ActivityAction.Update, |
| 1 | 392 | | }); |
| | 393 | |
|
| 1 | 394 | | return true; |
| 1 | 395 | | } |
| | 396 | |
|
| | 397 | | public async Task<bool> DeleteAutoTaskAsync(int autoTaskId) |
| 6 | 398 | | { |
| 6 | 399 | | var autoTask = await _context.AutoTasks.AsNoTracking() |
| 6 | 400 | | .Where(t => t.Id == autoTaskId) |
| 6 | 401 | | .FirstOrDefaultAsync() |
| 6 | 402 | | ?? throw new LgdxNotFound404Exception(); |
| 5 | 403 | | if (autoTask.CurrentProgressId != (int)ProgressState.Template) |
| 4 | 404 | | { |
| 4 | 405 | | throw new LgdxValidation400Expection("AutoTaskId", "Cannot delete the task not in running status."); |
| | 406 | | } |
| | 407 | |
|
| 1 | 408 | | _context.AutoTasks.Remove(autoTask); |
| 1 | 409 | | await _context.SaveChangesAsync(); |
| | 410 | |
|
| 1 | 411 | | await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel |
| 1 | 412 | | { |
| 1 | 413 | | EntityName = nameof(AutoTask), |
| 1 | 414 | | EntityId = autoTaskId.ToString(), |
| 1 | 415 | | Action = ActivityAction.Delete, |
| 1 | 416 | | }); |
| | 417 | |
|
| 1 | 418 | | return true; |
| 1 | 419 | | } |
| | 420 | |
|
| | 421 | | public async Task AbortAutoTaskAsync(int autoTaskId) |
| 4 | 422 | | { |
| 4 | 423 | | var autoTask = await _context.AutoTasks.AsNoTracking() |
| 4 | 424 | | .Where(t => t.Id == autoTaskId) |
| 4 | 425 | | .FirstOrDefaultAsync() |
| 4 | 426 | | ?? throw new LgdxNotFound404Exception(); |
| | 427 | |
|
| 3 | 428 | | if (autoTask.CurrentProgressId == (int)ProgressState.Template || |
| 3 | 429 | | autoTask.CurrentProgressId == (int)ProgressState.Completed || |
| 3 | 430 | | autoTask.CurrentProgressId == (int)ProgressState.Aborted) |
| 3 | 431 | | { |
| 3 | 432 | | throw new LgdxValidation400Expection(nameof(autoTaskId), "Cannot abort the task not in running status."); |
| | 433 | | } |
| | 434 | |
|
| 0 | 435 | | await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel |
| 0 | 436 | | { |
| 0 | 437 | | EntityName = nameof(AutoTask), |
| 0 | 438 | | EntityId = autoTaskId.ToString(), |
| 0 | 439 | | Action = ActivityAction.AutoTaskManualAbort, |
| 0 | 440 | | }); |
| | 441 | |
|
| 0 | 442 | | if (autoTask.CurrentProgressId != (int)ProgressState.Waiting && |
| 0 | 443 | | autoTask.AssignedRobotId != null && |
| 0 | 444 | | await _onlineRobotsService.SetAbortTaskAsync((Guid)autoTask.AssignedRobotId!)) |
| 0 | 445 | | { |
| | 446 | | // If the robot is online, abort the task from the robot |
| 0 | 447 | | return; |
| | 448 | | } |
| | 449 | | else |
| 0 | 450 | | { |
| 0 | 451 | | await _autoTaskSchedulerService.AutoTaskAbortApiAsync(autoTask.Id); |
| 0 | 452 | | } |
| 0 | 453 | | } |
| | 454 | |
|
| | 455 | | public async Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token) |
| 0 | 456 | | { |
| 0 | 457 | | var result = await _autoTaskSchedulerService.AutoTaskNextApiAsync(robotId, taskId, token); |
| 0 | 458 | | if (!result) |
| 0 | 459 | | { |
| 0 | 460 | | throw new LgdxValidation400Expection(nameof(token), "The next token is invalid."); |
| | 461 | | } |
| 0 | 462 | | } |
| | 463 | |
|
| | 464 | | public async Task<AutoTaskStatisticsBusinessModel> GetAutoTaskStatisticsAsync(int realmId) |
| 0 | 465 | | { |
| 0 | 466 | | _memoryCache.TryGetValue("Automation_Statistics", out AutoTaskStatisticsBusinessModel? autoTaskStatistics); |
| 0 | 467 | | if (autoTaskStatistics != null) |
| 0 | 468 | | { |
| 0 | 469 | | return autoTaskStatistics; |
| | 470 | | } |
| | 471 | |
|
| | 472 | | /* |
| | 473 | | * Get queuing tasks and running tasks (total) |
| | 474 | | */ |
| 0 | 475 | | DateTime CurrentDate = DateTime.UtcNow; |
| 0 | 476 | | var waitingTaskCount = await _context.AutoTasks.AsNoTracking() |
| 0 | 477 | | .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting) |
| 0 | 478 | | .CountAsync(); |
| 0 | 479 | | var waitingTaskPerHour = await _context.AutoTasksJourney.AsNoTracking() |
| 0 | 480 | | .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting && t.CreatedAt > DateTime.UtcNow.AddHours(-23)) |
| 0 | 481 | | .GroupBy(t => new |
| 0 | 482 | | { |
| 0 | 483 | | t.CreatedAt.Year, |
| 0 | 484 | | t.CreatedAt.Month, |
| 0 | 485 | | t.CreatedAt.Day, |
| 0 | 486 | | t.CreatedAt.Hour |
| 0 | 487 | | }) |
| 0 | 488 | | .Select(g => new |
| 0 | 489 | | { |
| 0 | 490 | | Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0), |
| 0 | 491 | | TasksCompleted = g.Count() |
| 0 | 492 | | }) |
| 0 | 493 | | .OrderBy(g => g.Time) |
| 0 | 494 | | .ToListAsync(); |
| 0 | 495 | | Dictionary<int, int> waitingTaskPerHourDict = []; |
| 0 | 496 | | foreach (var taskCount in waitingTaskPerHour) |
| 0 | 497 | | { |
| 0 | 498 | | waitingTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted); |
| 0 | 499 | | } |
| | 500 | |
|
| 0 | 501 | | var runningTaskCount = await _context.AutoTasks.AsNoTracking() |
| 0 | 502 | | .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!)) |
| 0 | 503 | | .CountAsync(); |
| 0 | 504 | | var runningTaskPerHour = await _context.AutoTasksJourney.AsNoTracking() |
| 0 | 505 | | .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains((int)t.CurrentProgressId!) && t.CreatedAt > DateTime.UtcNow.Ad |
| 0 | 506 | | .GroupBy(t => new |
| 0 | 507 | | { |
| 0 | 508 | | t.CreatedAt.Year, |
| 0 | 509 | | t.CreatedAt.Month, |
| 0 | 510 | | t.CreatedAt.Day, |
| 0 | 511 | | t.CreatedAt.Hour |
| 0 | 512 | | }) |
| 0 | 513 | | .Select(g => new |
| 0 | 514 | | { |
| 0 | 515 | | Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0), |
| 0 | 516 | | TasksCompleted = g.Count() |
| 0 | 517 | | }) |
| 0 | 518 | | .OrderBy(g => g.Time) |
| 0 | 519 | | .ToListAsync(); |
| 0 | 520 | | Dictionary<int, int> runningTaskPerHourDict = []; |
| 0 | 521 | | foreach (var taskCount in runningTaskPerHour) |
| 0 | 522 | | { |
| 0 | 523 | | runningTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted); |
| 0 | 524 | | } |
| | 525 | |
|
| | 526 | | /* |
| | 527 | | * Get task completed / aborted in the last 24 hours |
| | 528 | | */ |
| 0 | 529 | | var completedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking() |
| 0 | 530 | | .Where(t => t.CurrentProgressId == (int)ProgressState.Completed && t.CreatedAt > DateTime.UtcNow.AddHours(-23)) |
| 0 | 531 | | .GroupBy(t => new |
| 0 | 532 | | { |
| 0 | 533 | | t.CreatedAt.Year, |
| 0 | 534 | | t.CreatedAt.Month, |
| 0 | 535 | | t.CreatedAt.Day, |
| 0 | 536 | | t.CreatedAt.Hour |
| 0 | 537 | | }) |
| 0 | 538 | | .Select(g => new |
| 0 | 539 | | { |
| 0 | 540 | | Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0), |
| 0 | 541 | | TasksCompleted = g.Count() |
| 0 | 542 | | }) |
| 0 | 543 | | .OrderBy(g => g.Time) |
| 0 | 544 | | .ToListAsync(); |
| 0 | 545 | | Dictionary<int, int> completedTaskPerHourDict = []; |
| 0 | 546 | | foreach (var taskCount in completedTaskPerHour) |
| 0 | 547 | | { |
| 0 | 548 | | completedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted); |
| 0 | 549 | | } |
| | 550 | |
|
| 0 | 551 | | var abortedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking() |
| 0 | 552 | | .Where(t => t.CurrentProgressId == (int)ProgressState.Aborted && t.CreatedAt > DateTime.UtcNow.AddHours(-23)) |
| 0 | 553 | | .GroupBy(t => new |
| 0 | 554 | | { |
| 0 | 555 | | t.CreatedAt.Year, |
| 0 | 556 | | t.CreatedAt.Month, |
| 0 | 557 | | t.CreatedAt.Day, |
| 0 | 558 | | t.CreatedAt.Hour |
| 0 | 559 | | }) |
| 0 | 560 | | .Select(g => new |
| 0 | 561 | | { |
| 0 | 562 | | Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0), |
| 0 | 563 | | TasksCompleted = g.Count() |
| 0 | 564 | | }) |
| 0 | 565 | | .OrderBy(g => g.Time) |
| 0 | 566 | | .ToListAsync(); |
| 0 | 567 | | Dictionary<int, int> abortedTaskPerHourDict = []; |
| 0 | 568 | | foreach (var taskCount in abortedTaskPerHour) |
| 0 | 569 | | { |
| 0 | 570 | | abortedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted); |
| 0 | 571 | | } |
| | 572 | |
|
| 0 | 573 | | AutoTaskStatisticsBusinessModel statistics = new() |
| 0 | 574 | | { |
| 0 | 575 | | LastUpdatedAt = DateTime.Now, |
| 0 | 576 | | WaitingTasks = waitingTaskCount, |
| 0 | 577 | | RunningTasks = runningTaskCount, |
| 0 | 578 | | }; |
| | 579 | |
|
| | 580 | | /* |
| | 581 | | * Get trends |
| | 582 | | * Note that the completed tasks and aborted tasks are looked up in the last 24 hours |
| | 583 | | */ |
| | 584 | | // Now = total tasks |
| 0 | 585 | | statistics.WaitingTasksTrend.Add(waitingTaskCount); |
| 0 | 586 | | statistics.RunningTasksTrend.Add(runningTaskCount); |
| 0 | 587 | | for (int i = 0; i < 23; i++) |
| 0 | 588 | | { |
| 0 | 589 | | statistics.WaitingTasksTrend.Add(waitingTaskPerHourDict.TryGetValue(i, out int count) ? count : 0); |
| 0 | 590 | | statistics.RunningTasksTrend.Add(runningTaskPerHourDict.TryGetValue(i, out count) ? count : 0); |
| 0 | 591 | | statistics.CompletedTasksTrend.Add(completedTaskPerHourDict.TryGetValue(i, out count) ? count : 0); |
| 0 | 592 | | statistics.AbortedTasksTrend.Add(abortedTaskPerHourDict.TryGetValue(i, out count) ? count : 0); |
| 0 | 593 | | } |
| 0 | 594 | | statistics.WaitingTasksTrend.Reverse(); |
| 0 | 595 | | statistics.RunningTasksTrend.Reverse(); |
| 0 | 596 | | statistics.CompletedTasksTrend.Reverse(); |
| 0 | 597 | | statistics.CompletedTasks = statistics.CompletedTasksTrend.Sum(); |
| 0 | 598 | | statistics.AbortedTasksTrend.Reverse(); |
| 0 | 599 | | statistics.AbortedTasks = statistics.AbortedTasksTrend.Sum(); |
| 0 | 600 | | _memoryCache.Set("Automation_Statistics", statistics, TimeSpan.FromMinutes(5)); |
| | 601 | |
|
| 0 | 602 | | return statistics; |
| 0 | 603 | | } |
| | 604 | |
|
| | 605 | | public async Task<AutoTaskListBusinessModel?> GetRobotCurrentTaskAsync(Guid robotId) |
| 0 | 606 | | { |
| 0 | 607 | | var autoTask = await _context.AutoTasks.AsNoTracking() |
| 0 | 608 | | .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!) && t.AssignedRobotId == robotId) |
| 0 | 609 | | .Select(t => new AutoTaskListBusinessModel |
| 0 | 610 | | { |
| 0 | 611 | | Id = t.Id, |
| 0 | 612 | | Name = t.Name, |
| 0 | 613 | | Priority = t.Priority, |
| 0 | 614 | | FlowId = t.Flow!.Id, |
| 0 | 615 | | FlowName = t.Flow.Name, |
| 0 | 616 | | RealmId = t.Realm.Id, |
| 0 | 617 | | RealmName = t.Realm.Name, |
| 0 | 618 | | AssignedRobotId = t.AssignedRobotId, |
| 0 | 619 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 0 | 620 | | CurrentProgressId = t.CurrentProgressId, |
| 0 | 621 | | CurrentProgressName = t.CurrentProgress.Name, |
| 0 | 622 | | }) |
| 0 | 623 | | .FirstOrDefaultAsync(); |
| 0 | 624 | | return autoTask; |
| 0 | 625 | | } |
| | 626 | | } |