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