< Summary

Information
Class: LGDXRobotCloud.API.Services.Automation.AutoTaskService
Assembly: LGDXRobotCloud.API
File(s): /builds/yukaitung/lgdxrobot2-cloud/LGDXRobotCloud.API/Services/Automation/AutoTaskService.cs
Line coverage
68%
Covered lines: 350
Uncovered lines: 160
Coverable lines: 510
Total lines: 628
Line coverage: 68.6%
Branch coverage
62%
Covered branches: 65
Total branches: 104
Branch coverage: 62.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1212100%
GetAutoTasksAsync()100%1212100%
GetAutoTaskAsync()100%22100%
ValidateAutoTask()100%1212100%
CreateAutoTaskAsync()50%121291.66%
UpdateAutoTaskAsync()56.25%181680.85%
DeleteAutoTaskAsync()100%44100%
AbortAutoTaskAsync()100%1414100%
AutoTaskNextApiAsync()100%22100%
GetAutoTaskStatisticsAsync()0%420200%
GetRobotCurrentTaskAsync()100%210%

File(s)

/builds/yukaitung/lgdxrobot2-cloud/LGDXRobotCloud.API/Services/Automation/AutoTaskService.cs

#LineLine coverage
 1using LGDXRobotCloud.API.Exceptions;
 2using LGDXRobotCloud.API.Services.Administration;
 3using LGDXRobotCloud.API.Services.Common;
 4using LGDXRobotCloud.API.Services.Navigation;
 5using LGDXRobotCloud.Data.DbContexts;
 6using LGDXRobotCloud.Data.Entities;
 7using LGDXRobotCloud.Data.Models.Business.Administration;
 8using LGDXRobotCloud.Data.Models.Business.Automation;
 9using LGDXRobotCloud.Data.Models.Business.Navigation;
 10using LGDXRobotCloud.Utilities.Enums;
 11using LGDXRobotCloud.Utilities.Helpers;
 12using MassTransit;
 13using Microsoft.EntityFrameworkCore;
 14using Microsoft.Extensions.Caching.Memory;
 15
 16namespace LGDXRobotCloud.API.Services.Automation;
 17
 18public 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
 3533public class AutoTaskService(
 3534    IActivityLogService activityLogService,
 3535    IAutoTaskSchedulerService autoTaskSchedulerService,
 3536    IBus bus,
 3537    IEventService eventService,
 3538    IMemoryCache memoryCache,
 3539    IOnlineRobotsService onlineRobotsService,
 3540    LgdxContext context
 3541  ) : IAutoTaskService
 42{
 3543  private readonly IActivityLogService _activityLogService = activityLogService ?? throw new ArgumentNullException(nameo
 3544  private readonly IAutoTaskSchedulerService _autoTaskSchedulerService = autoTaskSchedulerService ?? throw new ArgumentN
 3545  private readonly IBus _bus = bus ?? throw new ArgumentNullException(nameof(bus));
 3546  private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
 3547  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 3548  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 49
 50  public async Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? 
 951  {
 952    var query = _context.AutoTasks as IQueryable<AutoTask>;
 953    if (!string.IsNullOrWhiteSpace(name))
 354    {
 355      name = name.Trim();
 356      query = query.Where(t => t.Name != null && t.Name.ToLower().Contains(name.ToLower()));
 357    }
 958    if (realmId != null)
 959    {
 960      query = query.Where(t => t.RealmId == realmId);
 961    }
 962    switch (autoTaskCatrgory)
 63    {
 64      case AutoTaskCatrgory.Template:
 165        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Template);
 166        break;
 67      case AutoTaskCatrgory.Waiting:
 168        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Waiting);
 169        break;
 70      case AutoTaskCatrgory.Completed:
 171        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Completed);
 172        break;
 73      case AutoTaskCatrgory.Aborted:
 174        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Aborted);
 175        break;
 76      case AutoTaskCatrgory.Running:
 177        query = query.Where(t => t.CurrentProgressId > (int)ProgressState.Aborted);
 178        break;
 79    }
 980    var itemCount = await query.CountAsync();
 981    var PaginationHelper = new PaginationHelper(itemCount, pageNumber, pageSize);
 982    var autoTasks = await query.AsNoTracking()
 983      .OrderByDescending(t => t.Priority)
 984      .ThenBy(t => t.Id)
 985      .Skip(pageSize * (pageNumber - 1))
 986      .Take(pageSize)
 987      .Select(t => new AutoTaskListBusinessModel
 988      {
 989        Id = t.Id,
 990        Name = t.Name,
 991        Priority = t.Priority,
 992        FlowId = t.Flow!.Id,
 993        FlowName = t.Flow.Name,
 994        RealmId = t.Realm.Id,
 995        RealmName = t.Realm.Name,
 996        AssignedRobotId = t.AssignedRobotId,
 997        AssignedRobotName = t.AssignedRobot!.Name,
 998        CurrentProgressId = t.CurrentProgressId,
 999        CurrentProgressName = t.CurrentProgress.Name,
 9100      })
 9101      .AsSplitQuery()
 9102      .ToListAsync();
 9103    return (autoTasks, PaginationHelper);
 9104  }
 105
 106  public async Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId)
 2107  {
 2108    return await _context.AutoTasks.AsNoTracking()
 2109      .Where(t => t.Id == autoTaskId)
 2110      .Include(t => t.AutoTaskDetails
 2111        .OrderBy(td => td.Order))
 2112        .ThenInclude(td => td.Waypoint)
 2113      .Include(t => t.AutoTaskJourneys)
 2114        .ThenInclude(tj => tj.CurrentProgress)
 2115      .Include(t => t.AssignedRobot)
 2116      .Include(t => t.CurrentProgress)
 2117      .Include(t => t.Flow)
 2118      .Include(t => t.Realm)
 2119      .AsSplitQuery()
 2120      .Select(t => new AutoTaskBusinessModel
 2121      {
 2122        Id = t.Id,
 2123        Name = t.Name,
 2124        Priority = t.Priority,
 2125        FlowId = t.Flow!.Id,
 2126        FlowName = t.Flow.Name,
 2127        RealmId = t.Realm.Id,
 2128        RealmName = t.Realm.Name,
 2129        AssignedRobotId = t.AssignedRobotId,
 2130        AssignedRobotName = t.AssignedRobot!.Name,
 2131        CurrentProgressId = t.CurrentProgressId,
 2132        CurrentProgressName = t.CurrentProgress.Name,
 2133        AutoTaskJourneys = t.AutoTaskJourneys.Select(tj => new AutoTaskJourneyBusinessModel
 2134        {
 2135          Id = tj.Id,
 2136          CurrentProcessId = tj.CurrentProgressId,
 2137          CurrentProcessName = tj.CurrentProgress == null ? null : tj.CurrentProgress.Name,
 2138          CreatedAt = tj.CreatedAt,
 2139        })
 2140        .OrderBy(tj => tj.Id)
 2141        .ToList(),
 2142        AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel
 2143        {
 2144          Id = td.Id,
 2145          Order = td.Order,
 2146          CustomX = td.CustomX,
 2147          CustomY = td.CustomY,
 2148          CustomRotation = td.CustomRotation,
 2149          Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel
 2150          {
 2151            Id = td.Waypoint.Id,
 2152            Name = td.Waypoint.Name,
 2153            RealmId = t.Realm.Id,
 2154            RealmName = t.Realm.Name,
 2155            X = td.Waypoint.X,
 2156            Y = td.Waypoint.Y,
 2157            Rotation = td.Waypoint.Rotation,
 2158            IsParking = td.Waypoint.IsParking,
 2159            HasCharger = td.Waypoint.HasCharger,
 2160            IsReserved = td.Waypoint.IsReserved,
 2161          },
 2162        })
 2163        .OrderBy(td => td.Order)
 2164        .ToList()
 2165      })
 2166      .FirstOrDefaultAsync()
 2167        ?? throw new LgdxNotFound404Exception();
 1168  }
 169
 170  private async Task ValidateAutoTask(HashSet<int> waypointIds, int flowId, int realmId, Guid? robotId)
 7171  {
 172    // Validate the Waypoint IDs
 7173    var waypointDict = await _context.Waypoints.AsNoTracking()
 7174      .Where(w => waypointIds.Contains(w.Id))
 7175      .Where(w => w.RealmId == realmId)
 11176      .ToDictionaryAsync(w => w.Id, w => w);
 26177    foreach (var waypointId in waypointIds)
 3178    {
 3179      if (!waypointDict.ContainsKey(waypointId))
 1180      {
 1181        throw new LgdxValidation400Expection(nameof(Waypoint), $"The Waypoint ID {waypointId} is invalid.");
 182      }
 2183    }
 184    // Validate the Flow ID
 6185    var flow = await _context.Flows.Where(f => f.Id == flowId).AnyAsync();
 6186    if (flow == false)
 1187    {
 1188      throw new LgdxValidation400Expection(nameof(Flow), $"The Flow ID {flowId} is invalid.");
 189    }
 190    // Validate the Realm ID
 5191    var realm = await _context.Realms.Where(r => r.Id == realmId).AnyAsync();
 5192    if (realm == false)
 1193    {
 1194      throw new LgdxValidation400Expection(nameof(Realm), $"The Realm ID {realmId} is invalid.");
 195    }
 196    // Validate the Assigned Robot ID
 4197    if (robotId != null)
 4198    {
 4199      var robot = await _context
 4200        .Robots
 4201        .Where(r => r.Id == robotId)
 4202        .Where(r => r.RealmId == realmId)
 4203        .AnyAsync();
 4204      if (robot == false)
 1205      {
 1206        throw new LgdxValidation400Expection(nameof(Robot), $"Robot ID: {robotId} is invalid.");
 207      }
 3208    }
 3209  }
 210
 211  public async Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel)
 6212  {
 213    // Waypoint is needed when Waypoins Traffic Control is enabled
 6214    bool hasWaypointsTrafficControl = await _context.Realms.AsNoTracking()
 6215      .Where(r => r.Id == autoTaskCreateBusinessModel.RealmId)
 6216      .Select(r => r.HasWaypointsTrafficControl)
 6217      .FirstOrDefaultAsync();
 6218    if (hasWaypointsTrafficControl)
 0219    {
 0220      foreach (var detail in autoTaskCreateBusinessModel.AutoTaskDetails)
 0221      {
 0222        if (detail.WaypointId == null)
 0223        {
 0224          throw new LgdxValidation400Expection(nameof(detail.WaypointId), "Waypoint is required when Waypoints Traffic C
 225        }
 0226      }
 0227    }
 228
 6229    HashSet<int> waypointIds = autoTaskCreateBusinessModel.AutoTaskDetails
 3230      .Where(d => d.WaypointId != null)
 3231      .Select(d => d.WaypointId!.Value)
 6232      .ToHashSet();
 233
 6234    await ValidateAutoTask(waypointIds,
 6235      autoTaskCreateBusinessModel.FlowId,
 6236      autoTaskCreateBusinessModel.RealmId,
 6237      autoTaskCreateBusinessModel.AssignedRobotId);
 238
 2239    var autoTask = new AutoTask
 2240    {
 2241      Name = autoTaskCreateBusinessModel.Name,
 2242      Priority = autoTaskCreateBusinessModel.Priority,
 2243      FlowId = autoTaskCreateBusinessModel.FlowId,
 2244      RealmId = autoTaskCreateBusinessModel.RealmId,
 2245      AssignedRobotId = autoTaskCreateBusinessModel.AssignedRobotId,
 2246      CurrentProgressId = autoTaskCreateBusinessModel.IsTemplate
 2247        ? (int)ProgressState.Template
 2248        : (int)ProgressState.Waiting,
 2249      AutoTaskDetails = autoTaskCreateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail
 2250      {
 2251        Order = td.Order,
 2252        CustomX = td.CustomX,
 2253        CustomY = td.CustomY,
 2254        CustomRotation = td.CustomRotation,
 2255        WaypointId = td.WaypointId,
 2256      })
 0257      .OrderBy(td => td.Order)
 2258      .ToList(),
 2259    };
 2260    await _context.AutoTasks.AddAsync(autoTask);
 2261    await _context.SaveChangesAsync();
 2262    _autoTaskSchedulerService.ResetIgnoreRobot(autoTask.RealmId);
 2263    eventService.AutoTaskHasCreated();
 264
 2265    var autoTaskBusinessModel = await _context.AutoTasks.AsNoTracking()
 2266      .Where(t => t.Id == autoTask.Id)
 2267      .Select(t => new AutoTaskBusinessModel
 2268      {
 2269        Id = t.Id,
 2270        Name = t.Name,
 2271        Priority = t.Priority,
 2272        FlowId = t.Flow!.Id,
 2273        FlowName = t.Flow.Name,
 2274        RealmId = t.Realm.Id,
 2275        RealmName = t.Realm.Name,
 2276        AssignedRobotId = t.AssignedRobotId,
 2277        AssignedRobotName = t.AssignedRobot!.Name,
 2278        CurrentProgressId = t.CurrentProgressId,
 2279        CurrentProgressName = t.CurrentProgress.Name,
 2280        AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel
 2281        {
 2282          Id = td.Id,
 2283          Order = td.Order,
 2284          CustomX = td.CustomX,
 2285          CustomY = td.CustomY,
 2286          CustomRotation = td.CustomRotation,
 2287          Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel
 2288          {
 2289            Id = td.Waypoint.Id,
 2290            Name = td.Waypoint.Name,
 2291            RealmId = t.Realm.Id,
 2292            RealmName = t.Realm.Name,
 2293            X = td.Waypoint.X,
 2294            Y = td.Waypoint.Y,
 2295            Rotation = td.Waypoint.Rotation,
 2296            IsParking = td.Waypoint.IsParking,
 2297            HasCharger = td.Waypoint.HasCharger,
 2298            IsReserved = td.Waypoint.IsReserved,
 2299          },
 2300        })
 2301        .OrderBy(td => td.Order)
 2302        .ToList()
 2303      })
 2304      .FirstOrDefaultAsync()
 2305        ?? throw new LgdxNotFound404Exception();
 306
 2307    if (autoTask.CurrentProgressId == (int)ProgressState.Waiting)
 1308    {
 1309      var autoTaskJourney = new AutoTaskJourney
 1310      {
 1311        AutoTaskId = autoTaskBusinessModel.Id,
 1312        CurrentProgressId = autoTask.CurrentProgressId
 1313      };
 1314      await _context.AutoTasksJourney.AddAsync(autoTaskJourney);
 1315      await _context.SaveChangesAsync();
 316
 1317      await _bus.Publish(autoTaskBusinessModel.ToContract());
 1318    }
 319
 2320    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 2321    {
 2322      EntityName = nameof(AutoTask),
 2323      EntityId = autoTaskBusinessModel.Id.ToString(),
 2324      Action = ActivityAction.Create,
 2325    });
 326
 2327    return autoTaskBusinessModel;
 2328  }
 329
 330  public async Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel)
 4331  {
 4332    var task = await _context.AutoTasks
 4333      .Where(t => t.Id == autoTaskId)
 4334      .Include(t => t.AutoTaskDetails
 4335        .OrderBy(td => td.Order))
 4336      .FirstOrDefaultAsync()
 4337      ?? throw new LgdxNotFound404Exception();
 338
 3339    if (task.CurrentProgressId != (int)ProgressState.Template)
 1340    {
 1341      throw new LgdxValidation400Expection("AutoTaskId", "Only AutoTask Templates are editable.");
 342    }
 343    // Ensure the input task does not include Detail ID from other task
 6344    HashSet<int> dbDetailIds = task.AutoTaskDetails.Select(d => d.Id).ToHashSet();
 9345    foreach (var bmDetailId in autoTaskUpdateBusinessModel.AutoTaskDetails.Where(d => d.Id != null).Select(d => d.Id))
 1346    {
 1347      if (bmDetailId != null && !dbDetailIds.Contains((int)bmDetailId))
 1348      {
 1349        throw new LgdxValidation400Expection("AutoTaskDetailId", $"The Task Detail ID {(int)bmDetailId} is belongs to ot
 350      }
 0351    }
 352    // Waypoint is needed when Waypoins Traffic Control is enabled
 1353    bool hasWaypointsTrafficControl = await _context.Realms.AsNoTracking()
 1354      .Where(r => r.Id == task.RealmId)
 1355      .Select(r => r.HasWaypointsTrafficControl)
 1356      .FirstOrDefaultAsync();
 1357    if (hasWaypointsTrafficControl)
 0358    {
 0359      foreach (var detail in autoTaskUpdateBusinessModel.AutoTaskDetails)
 0360      {
 0361        if (detail.WaypointId == null)
 0362        {
 0363          throw new LgdxValidation400Expection(nameof(detail.WaypointId), "Waypoint is required when Waypoints Traffic C
 364        }
 0365      }
 0366    }
 1367    HashSet<int> waypointIds = autoTaskUpdateBusinessModel.AutoTaskDetails
 0368      .Where(d => d.WaypointId != null)
 0369      .Select(d => d.WaypointId!.Value)
 1370      .ToHashSet();
 1371    await ValidateAutoTask(waypointIds,
 1372      autoTaskUpdateBusinessModel.FlowId,
 1373      task.RealmId,
 1374      autoTaskUpdateBusinessModel.AssignedRobotId);
 375
 1376    task.Name = autoTaskUpdateBusinessModel.Name;
 1377    task.Priority = autoTaskUpdateBusinessModel.Priority;
 1378    task.FlowId = autoTaskUpdateBusinessModel.FlowId;
 1379    task.AssignedRobotId = autoTaskUpdateBusinessModel.AssignedRobotId;
 1380    task.AutoTaskDetails = autoTaskUpdateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail
 1381    {
 1382      Id = (int)td.Id!,
 1383      Order = td.Order,
 1384      CustomX = td.CustomX,
 1385      CustomY = td.CustomY,
 1386      CustomRotation = td.CustomRotation,
 1387      WaypointId = td.WaypointId,
 1388    }).ToList();
 1389    await _context.SaveChangesAsync();
 390
 1391    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1392    {
 1393      EntityName = nameof(AutoTask),
 1394      EntityId = autoTaskId.ToString(),
 1395      Action = ActivityAction.Update,
 1396    });
 397
 1398    return true;
 1399  }
 400
 401  public async Task<bool> DeleteAutoTaskAsync(int autoTaskId)
 6402  {
 6403    var autoTask = await _context.AutoTasks.AsNoTracking()
 6404      .Where(t => t.Id == autoTaskId)
 6405      .FirstOrDefaultAsync()
 6406      ?? throw new LgdxNotFound404Exception();
 5407    if (autoTask.CurrentProgressId != (int)ProgressState.Template)
 4408    {
 4409      throw new LgdxValidation400Expection("AutoTaskId", "Cannot delete the task not in running status.");
 410    }
 411
 1412    _context.AutoTasks.Remove(autoTask);
 1413    await _context.SaveChangesAsync();
 414
 1415    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1416    {
 1417      EntityName = nameof(AutoTask),
 1418      EntityId = autoTaskId.ToString(),
 1419      Action = ActivityAction.Delete,
 1420    });
 421
 1422    return true;
 1423  }
 424
 425  public async Task AbortAutoTaskAsync(int autoTaskId)
 6426  {
 6427    var autoTask = await _context.AutoTasks.AsNoTracking()
 6428      .Where(t => t.Id == autoTaskId)
 6429      .FirstOrDefaultAsync()
 6430      ?? throw new LgdxNotFound404Exception();
 431
 5432    if (autoTask.CurrentProgressId == (int)ProgressState.Template ||
 5433        autoTask.CurrentProgressId == (int)ProgressState.Completed ||
 5434        autoTask.CurrentProgressId == (int)ProgressState.Aborted)
 3435    {
 3436      throw new LgdxValidation400Expection(nameof(autoTaskId), "Cannot abort the task not in running status.");
 437    }
 438
 2439    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 2440    {
 2441      EntityName = nameof(AutoTask),
 2442      EntityId = autoTaskId.ToString(),
 2443      Action = ActivityAction.AutoTaskManualAbort,
 2444    });
 445
 2446    if (autoTask.CurrentProgressId != (int)ProgressState.Waiting &&
 2447        autoTask.AssignedRobotId != null &&
 2448        await _onlineRobotsService.SetAbortTaskAsync((Guid)autoTask.AssignedRobotId!, true))
 1449    {
 450      // If the robot is online, abort the task from the robot
 1451      return;
 452    }
 453    else
 1454    {
 1455      await _autoTaskSchedulerService.AutoTaskAbortApiAsync(autoTask.Id);
 1456    }
 2457  }
 458
 459  public async Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 2460  {
 2461    var result = await _autoTaskSchedulerService.AutoTaskNextApiAsync(robotId, taskId, token)
 2462      ?? throw new LgdxValidation400Expection(nameof(token), "The next token is invalid.");
 1463    _onlineRobotsService.SetAutoTaskNextApi(robotId, result);
 1464  }
 465
 466  public async Task<AutoTaskStatisticsBusinessModel> GetAutoTaskStatisticsAsync(int realmId)
 0467  {
 0468    _memoryCache.TryGetValue("Automation_Statistics", out AutoTaskStatisticsBusinessModel? autoTaskStatistics);
 0469    if (autoTaskStatistics != null)
 0470    {
 0471      return autoTaskStatistics;
 472    }
 473
 474    /*
 475     * Get queuing tasks and running tasks (total)
 476     */
 0477    DateTime CurrentDate = DateTime.UtcNow;
 0478    var waitingTaskCount = await _context.AutoTasks.AsNoTracking()
 0479      .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting)
 0480      .CountAsync();
 0481    var waitingTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0482    .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0483    .GroupBy(t => new
 0484    {
 0485      t.CreatedAt.Year,
 0486      t.CreatedAt.Month,
 0487      t.CreatedAt.Day,
 0488      t.CreatedAt.Hour
 0489    })
 0490    .Select(g => new
 0491    {
 0492      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0493      TasksCompleted = g.Count()
 0494    })
 0495    .OrderBy(g => g.Time)
 0496    .ToListAsync();
 0497    Dictionary<int, int> waitingTaskPerHourDict = [];
 0498    foreach (var taskCount in waitingTaskPerHour)
 0499    {
 0500      waitingTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0501    }
 502
 0503    var runningTaskCount = await _context.AutoTasks.AsNoTracking()
 0504      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!))
 0505      .CountAsync();
 0506    var runningTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0507    .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains((int)t.CurrentProgressId!) && t.CreatedAt > DateTime.UtcNow.Ad
 0508    .GroupBy(t => new
 0509    {
 0510      t.CreatedAt.Year,
 0511      t.CreatedAt.Month,
 0512      t.CreatedAt.Day,
 0513      t.CreatedAt.Hour
 0514    })
 0515    .Select(g => new
 0516    {
 0517      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0518      TasksCompleted = g.Count()
 0519    })
 0520    .OrderBy(g => g.Time)
 0521    .ToListAsync();
 0522    Dictionary<int, int> runningTaskPerHourDict = [];
 0523    foreach (var taskCount in runningTaskPerHour)
 0524    {
 0525      runningTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0526    }
 527
 528    /*
 529     * Get task completed / aborted in the last 24 hours
 530     */
 0531    var completedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0532    .Where(t => t.CurrentProgressId == (int)ProgressState.Completed && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0533    .GroupBy(t => new
 0534    {
 0535      t.CreatedAt.Year,
 0536      t.CreatedAt.Month,
 0537      t.CreatedAt.Day,
 0538      t.CreatedAt.Hour
 0539    })
 0540    .Select(g => new
 0541    {
 0542      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0543      TasksCompleted = g.Count()
 0544    })
 0545    .OrderBy(g => g.Time)
 0546    .ToListAsync();
 0547    Dictionary<int, int> completedTaskPerHourDict = [];
 0548    foreach (var taskCount in completedTaskPerHour)
 0549    {
 0550      completedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0551    }
 552
 0553    var abortedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0554    .Where(t => t.CurrentProgressId == (int)ProgressState.Aborted && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0555    .GroupBy(t => new
 0556    {
 0557      t.CreatedAt.Year,
 0558      t.CreatedAt.Month,
 0559      t.CreatedAt.Day,
 0560      t.CreatedAt.Hour
 0561    })
 0562    .Select(g => new
 0563    {
 0564      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0565      TasksCompleted = g.Count()
 0566    })
 0567    .OrderBy(g => g.Time)
 0568    .ToListAsync();
 0569    Dictionary<int, int> abortedTaskPerHourDict = [];
 0570    foreach (var taskCount in abortedTaskPerHour)
 0571    {
 0572      abortedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0573    }
 574
 0575    AutoTaskStatisticsBusinessModel statistics = new()
 0576    {
 0577      LastUpdatedAt = DateTime.Now,
 0578      WaitingTasks = waitingTaskCount,
 0579      RunningTasks = runningTaskCount,
 0580    };
 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
 0587    statistics.WaitingTasksTrend.Add(waitingTaskCount);
 0588    statistics.RunningTasksTrend.Add(runningTaskCount);
 0589    for (int i = 0; i < 23; i++)
 0590    {
 0591      statistics.WaitingTasksTrend.Add(waitingTaskPerHourDict.TryGetValue(i, out int count) ? count : 0);
 0592      statistics.RunningTasksTrend.Add(runningTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0593      statistics.CompletedTasksTrend.Add(completedTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0594      statistics.AbortedTasksTrend.Add(abortedTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0595    }
 0596    statistics.WaitingTasksTrend.Reverse();
 0597    statistics.RunningTasksTrend.Reverse();
 0598    statistics.CompletedTasksTrend.Reverse();
 0599    statistics.CompletedTasks = statistics.CompletedTasksTrend.Sum();
 0600    statistics.AbortedTasksTrend.Reverse();
 0601    statistics.AbortedTasks = statistics.AbortedTasksTrend.Sum();
 0602    _memoryCache.Set("Automation_Statistics", statistics, TimeSpan.FromMinutes(5));
 603
 0604    return statistics;
 0605  }
 606
 607  public async Task<AutoTaskListBusinessModel?> GetRobotCurrentTaskAsync(Guid robotId)
 0608  {
 0609    var autoTask = await _context.AutoTasks.AsNoTracking()
 0610      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!) && t.AssignedRobotId == robotId)
 0611      .Select(t => new AutoTaskListBusinessModel
 0612      {
 0613        Id = t.Id,
 0614        Name = t.Name,
 0615        Priority = t.Priority,
 0616        FlowId = t.Flow!.Id,
 0617        FlowName = t.Flow.Name,
 0618        RealmId = t.Realm.Id,
 0619        RealmName = t.Realm.Name,
 0620        AssignedRobotId = t.AssignedRobotId,
 0621        AssignedRobotName = t.AssignedRobot!.Name,
 0622        CurrentProgressId = t.CurrentProgressId,
 0623        CurrentProgressName = t.CurrentProgress.Name,
 0624      })
 0625      .FirstOrDefaultAsync();
 0626    return autoTask;
 0627  }
 628}