< 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
64%
Covered lines: 328
Uncovered lines: 181
Coverable lines: 509
Total lines: 626
Line coverage: 64.4%
Branch coverage
53%
Covered branches: 56
Total branches: 104
Branch coverage: 53.8%
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.57%
UpdateAutoTaskAsync()56.25%181680.85%
DeleteAutoTaskAsync()100%44100%
AbortAutoTaskAsync()50%561440%
AutoTaskNextApiAsync()0%620%
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.Repositories;
 3using LGDXRobotCloud.API.Services.Administration;
 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 Microsoft.EntityFrameworkCore;
 13using Microsoft.Extensions.Caching.Memory;
 14
 15namespace LGDXRobotCloud.API.Services.Automation;
 16
 17public 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
 3132public class AutoTaskService(
 3133    IActivityLogService activityLogService,
 3134    IAutoTaskRepository autoTaskRepository,
 3135    IAutoTaskSchedulerService autoTaskSchedulerService,
 3136    IMemoryCache memoryCache,
 3137    IOnlineRobotsService onlineRobotsService,
 3138    LgdxContext context
 3139  ) : IAutoTaskService
 40{
 3141  private readonly IActivityLogService _activityLogService = activityLogService ?? throw new ArgumentNullException(nameo
 3142  private readonly IAutoTaskRepository _autoTaskRepository = autoTaskRepository ?? throw new ArgumentNullException(nameo
 3143  private readonly IAutoTaskSchedulerService _autoTaskSchedulerService = autoTaskSchedulerService ?? throw new ArgumentN
 3144  private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
 3145  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 3146  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 47
 48  public async Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? 
 949  {
 950    var query = _context.AutoTasks as IQueryable<AutoTask>;
 951    if (!string.IsNullOrWhiteSpace(name))
 352    {
 353      name = name.Trim();
 354      query = query.Where(t => t.Name != null && t.Name.ToLower().Contains(name.ToLower()));
 355    }
 956    if (realmId != null)
 957    {
 958      query = query.Where(t => t.RealmId == realmId);
 959    }
 960    switch (autoTaskCatrgory)
 61    {
 62      case AutoTaskCatrgory.Template:
 163        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Template);
 164        break;
 65      case AutoTaskCatrgory.Waiting:
 166        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Waiting);
 167        break;
 68      case AutoTaskCatrgory.Completed:
 169        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Completed);
 170        break;
 71      case AutoTaskCatrgory.Aborted:
 172        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Aborted);
 173        break;
 74      case AutoTaskCatrgory.Running:
 175        query = query.Where(t => t.CurrentProgressId > (int)ProgressState.Aborted);
 176        break;
 77    }
 978    var itemCount = await query.CountAsync();
 979    var PaginationHelper = new PaginationHelper(itemCount, pageNumber, pageSize);
 980    var autoTasks = await query.AsNoTracking()
 981      .OrderByDescending(t => t.Priority)
 982      .ThenBy(t => t.Id)
 983      .Skip(pageSize * (pageNumber - 1))
 984      .Take(pageSize)
 985      .Select(t => new AutoTaskListBusinessModel
 986      {
 987        Id = t.Id,
 988        Name = t.Name,
 989        Priority = t.Priority,
 990        FlowId = t.Flow!.Id,
 991        FlowName = t.Flow.Name,
 992        RealmId = t.Realm.Id,
 993        RealmName = t.Realm.Name,
 994        AssignedRobotId = t.AssignedRobotId,
 995        AssignedRobotName = t.AssignedRobot!.Name,
 996        CurrentProgressId = t.CurrentProgressId,
 997        CurrentProgressName = t.CurrentProgress.Name,
 998      })
 999      .AsSplitQuery()
 9100      .ToListAsync();
 9101    return (autoTasks, PaginationHelper);
 9102  }
 103
 104  public async Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId)
 2105  {
 2106    return await _context.AutoTasks.AsNoTracking()
 2107      .Where(t => t.Id == autoTaskId)
 2108      .Include(t => t.AutoTaskDetails
 2109        .OrderBy(td => td.Order))
 2110        .ThenInclude(td => td.Waypoint)
 2111      .Include(t => t.AutoTaskJourneys)
 2112        .ThenInclude(tj => tj.CurrentProgress)
 2113      .Include(t => t.AssignedRobot)
 2114      .Include(t => t.CurrentProgress)
 2115      .Include(t => t.Flow)
 2116      .Include(t => t.Realm)
 2117      .AsSplitQuery()
 2118      .Select(t => new AutoTaskBusinessModel
 2119      {
 2120        Id = t.Id,
 2121        Name = t.Name,
 2122        Priority = t.Priority,
 2123        FlowId = t.Flow!.Id,
 2124        FlowName = t.Flow.Name,
 2125        RealmId = t.Realm.Id,
 2126        RealmName = t.Realm.Name,
 2127        AssignedRobotId = t.AssignedRobotId,
 2128        AssignedRobotName = t.AssignedRobot!.Name,
 2129        CurrentProgressId = t.CurrentProgressId,
 2130        CurrentProgressName = t.CurrentProgress.Name,
 2131        AutoTaskJourneys = t.AutoTaskJourneys.Select(tj => new AutoTaskJourneyBusinessModel
 2132        {
 2133          Id = tj.Id,
 2134          CurrentProcessId = tj.CurrentProgressId,
 2135          CurrentProcessName = tj.CurrentProgress == null ? null : tj.CurrentProgress.Name,
 2136          CreatedAt = tj.CreatedAt,
 2137        })
 2138        .OrderBy(tj => tj.Id)
 2139        .ToList(),
 2140        AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel
 2141        {
 2142          Id = td.Id,
 2143          Order = td.Order,
 2144          CustomX = td.CustomX,
 2145          CustomY = td.CustomY,
 2146          CustomRotation = td.CustomRotation,
 2147          Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel
 2148          {
 2149            Id = td.Waypoint.Id,
 2150            Name = td.Waypoint.Name,
 2151            RealmId = t.Realm.Id,
 2152            RealmName = t.Realm.Name,
 2153            X = td.Waypoint.X,
 2154            Y = td.Waypoint.Y,
 2155            Rotation = td.Waypoint.Rotation,
 2156            IsParking = td.Waypoint.IsParking,
 2157            HasCharger = td.Waypoint.HasCharger,
 2158            IsReserved = td.Waypoint.IsReserved,
 2159          },
 2160        })
 2161        .OrderBy(td => td.Order)
 2162        .ToList()
 2163      })
 2164      .FirstOrDefaultAsync()
 2165        ?? throw new LgdxNotFound404Exception();
 1166  }
 167
 168  private async Task ValidateAutoTask(HashSet<int> waypointIds, int flowId, int realmId, Guid? robotId)
 7169  {
 170    // Validate the Waypoint IDs
 7171    var waypointDict = await _context.Waypoints.AsNoTracking()
 7172      .Where(w => waypointIds.Contains(w.Id))
 7173      .Where(w => w.RealmId == realmId)
 11174      .ToDictionaryAsync(w => w.Id, w => w);
 26175    foreach (var waypointId in waypointIds)
 3176    {
 3177      if (!waypointDict.ContainsKey(waypointId))
 1178      {
 1179        throw new LgdxValidation400Expection(nameof(Waypoint), $"The Waypoint ID {waypointId} is invalid.");
 180      }
 2181    }
 182    // Validate the Flow ID
 6183    var flow = await _context.Flows.Where(f => f.Id == flowId).AnyAsync();
 6184    if (flow == false)
 1185    {
 1186      throw new LgdxValidation400Expection(nameof(Flow), $"The Flow ID {flowId} is invalid.");
 187    }
 188    // Validate the Realm ID
 5189    var realm = await _context.Realms.Where(r => r.Id == realmId).AnyAsync();
 5190    if (realm == false)
 1191    {
 1192      throw new LgdxValidation400Expection(nameof(Realm), $"The Realm ID {realmId} is invalid.");
 193    }
 194    // Validate the Assigned Robot ID
 4195    if (robotId != null)
 4196    {
 4197      var robot = await _context
 4198        .Robots
 4199        .Where(r => r.Id == robotId)
 4200        .Where(r => r.RealmId == realmId)
 4201        .AnyAsync();
 4202      if (robot == false)
 1203      {
 1204        throw new LgdxValidation400Expection(nameof(Robot), $"Robot ID: {robotId} is invalid.");
 205      }
 3206    }
 3207  }
 208
 209  public async Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel)
 6210  {
 211    // Waypoint is needed when Waypoins Traffic Control is enabled
 6212    bool hasWaypointsTrafficControl = await _context.Realms.AsNoTracking()
 6213      .Where(r => r.Id == autoTaskCreateBusinessModel.RealmId)
 6214      .Select(r => r.HasWaypointsTrafficControl)
 6215      .FirstOrDefaultAsync();
 6216    if (hasWaypointsTrafficControl)
 0217    {
 0218      foreach (var detail in autoTaskCreateBusinessModel.AutoTaskDetails)
 0219      {
 0220        if (detail.WaypointId == null)
 0221        {
 0222          throw new LgdxValidation400Expection(nameof(detail.WaypointId), "Waypoint is required when Waypoints Traffic C
 223        }
 0224      }
 0225    }
 226
 6227    HashSet<int> waypointIds = autoTaskCreateBusinessModel.AutoTaskDetails
 3228      .Where(d => d.WaypointId != null)
 3229      .Select(d => d.WaypointId!.Value)
 6230      .ToHashSet();
 231
 6232    await ValidateAutoTask(waypointIds,
 6233      autoTaskCreateBusinessModel.FlowId,
 6234      autoTaskCreateBusinessModel.RealmId,
 6235      autoTaskCreateBusinessModel.AssignedRobotId);
 236
 2237    var autoTask = new AutoTask
 2238    {
 2239      Name = autoTaskCreateBusinessModel.Name,
 2240      Priority = autoTaskCreateBusinessModel.Priority,
 2241      FlowId = autoTaskCreateBusinessModel.FlowId,
 2242      RealmId = autoTaskCreateBusinessModel.RealmId,
 2243      AssignedRobotId = autoTaskCreateBusinessModel.AssignedRobotId,
 2244      CurrentProgressId = autoTaskCreateBusinessModel.IsTemplate
 2245        ? (int)ProgressState.Template
 2246        : (int)ProgressState.Waiting,
 2247      AutoTaskDetails = autoTaskCreateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail
 2248      {
 2249        Order = td.Order,
 2250        CustomX = td.CustomX,
 2251        CustomY = td.CustomY,
 2252        CustomRotation = td.CustomRotation,
 2253        WaypointId = td.WaypointId,
 2254      })
 0255      .OrderBy(td => td.Order)
 2256      .ToList(),
 2257    };
 2258    await _context.AutoTasks.AddAsync(autoTask);
 2259    await _context.SaveChangesAsync();
 260
 2261    var autoTaskBusinessModel = await _context.AutoTasks.AsNoTracking()
 2262      .Where(t => t.Id == autoTask.Id)
 2263      .Select(t => new AutoTaskBusinessModel
 2264      {
 2265        Id = t.Id,
 2266        Name = t.Name,
 2267        Priority = t.Priority,
 2268        FlowId = t.Flow!.Id,
 2269        FlowName = t.Flow.Name,
 2270        RealmId = t.Realm.Id,
 2271        RealmName = t.Realm.Name,
 2272        AssignedRobotId = t.AssignedRobotId,
 2273        AssignedRobotName = t.AssignedRobot!.Name,
 2274        CurrentProgressId = t.CurrentProgressId,
 2275        CurrentProgressName = t.CurrentProgress.Name,
 2276        AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel
 2277        {
 2278          Id = td.Id,
 2279          Order = td.Order,
 2280          CustomX = td.CustomX,
 2281          CustomY = td.CustomY,
 2282          CustomRotation = td.CustomRotation,
 2283          Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel
 2284          {
 2285            Id = td.Waypoint.Id,
 2286            Name = td.Waypoint.Name,
 2287            RealmId = t.Realm.Id,
 2288            RealmName = t.Realm.Name,
 2289            X = td.Waypoint.X,
 2290            Y = td.Waypoint.Y,
 2291            Rotation = td.Waypoint.Rotation,
 2292            IsParking = td.Waypoint.IsParking,
 2293            HasCharger = td.Waypoint.HasCharger,
 2294            IsReserved = td.Waypoint.IsReserved,
 2295          },
 2296        })
 2297        .OrderBy(td => td.Order)
 2298        .ToList()
 2299      })
 2300      .FirstOrDefaultAsync()
 2301        ?? throw new LgdxNotFound404Exception();
 302
 2303    if (autoTask.CurrentProgressId == (int)ProgressState.Waiting)
 1304    {
 1305      var autoTaskJourney = new AutoTaskJourney
 1306      {
 1307        AutoTaskId = autoTaskBusinessModel.Id,
 1308        CurrentProgressId = autoTask.CurrentProgressId
 1309      };
 1310      await _context.AutoTasksJourney.AddAsync(autoTaskJourney);
 1311      await _context.SaveChangesAsync();
 1312      await _autoTaskRepository.AutoTaskHasUpdateAsync(autoTask.RealmId, autoTaskBusinessModel.ToContract());
 1313      await _autoTaskSchedulerService.RunSchedulerNewAutoTaskAsync(autoTask.RealmId, autoTask.AssignedRobotId);
 1314    }
 315
 2316    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 2317    {
 2318      EntityName = nameof(AutoTask),
 2319      EntityId = autoTaskBusinessModel.Id.ToString(),
 2320      Action = ActivityAction.Create,
 2321    });
 322
 2323    return autoTaskBusinessModel;
 2324  }
 325
 326  public async Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel)
 4327  {
 4328    var task = await _context.AutoTasks
 4329      .Where(t => t.Id == autoTaskId)
 4330      .Include(t => t.AutoTaskDetails
 4331        .OrderBy(td => td.Order))
 4332      .FirstOrDefaultAsync()
 4333      ?? throw new LgdxNotFound404Exception();
 334
 3335    if (task.CurrentProgressId != (int)ProgressState.Template)
 1336    {
 1337      throw new LgdxValidation400Expection("AutoTaskId", "Only AutoTask Templates are editable.");
 338    }
 339    // Ensure the input task does not include Detail ID from other task
 6340    HashSet<int> dbDetailIds = task.AutoTaskDetails.Select(d => d.Id).ToHashSet();
 9341    foreach (var bmDetailId in autoTaskUpdateBusinessModel.AutoTaskDetails.Where(d => d.Id != null).Select(d => d.Id))
 1342    {
 1343      if (bmDetailId != null && !dbDetailIds.Contains((int)bmDetailId))
 1344      {
 1345        throw new LgdxValidation400Expection("AutoTaskDetailId", $"The Task Detail ID {(int)bmDetailId} is belongs to ot
 346      }
 0347    }
 348    // Waypoint is needed when Waypoins Traffic Control is enabled
 1349    bool hasWaypointsTrafficControl = await _context.Realms.AsNoTracking()
 1350      .Where(r => r.Id == task.RealmId)
 1351      .Select(r => r.HasWaypointsTrafficControl)
 1352      .FirstOrDefaultAsync();
 1353    if (hasWaypointsTrafficControl)
 0354    {
 0355      foreach (var detail in autoTaskUpdateBusinessModel.AutoTaskDetails)
 0356      {
 0357        if (detail.WaypointId == null)
 0358        {
 0359          throw new LgdxValidation400Expection(nameof(detail.WaypointId), "Waypoint is required when Waypoints Traffic C
 360        }
 0361      }
 0362    }
 1363    HashSet<int> waypointIds = autoTaskUpdateBusinessModel.AutoTaskDetails
 0364      .Where(d => d.WaypointId != null)
 0365      .Select(d => d.WaypointId!.Value)
 1366      .ToHashSet();
 1367    await ValidateAutoTask(waypointIds,
 1368      autoTaskUpdateBusinessModel.FlowId,
 1369      task.RealmId,
 1370      autoTaskUpdateBusinessModel.AssignedRobotId);
 371
 1372    task.Name = autoTaskUpdateBusinessModel.Name;
 1373    task.Priority = autoTaskUpdateBusinessModel.Priority;
 1374    task.FlowId = autoTaskUpdateBusinessModel.FlowId;
 1375    task.AssignedRobotId = autoTaskUpdateBusinessModel.AssignedRobotId;
 1376    task.AutoTaskDetails = autoTaskUpdateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail
 1377    {
 1378      Id = (int)td.Id!,
 1379      Order = td.Order,
 1380      CustomX = td.CustomX,
 1381      CustomY = td.CustomY,
 1382      CustomRotation = td.CustomRotation,
 1383      WaypointId = td.WaypointId,
 1384    }).ToList();
 1385    await _context.SaveChangesAsync();
 386
 1387    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1388    {
 1389      EntityName = nameof(AutoTask),
 1390      EntityId = autoTaskId.ToString(),
 1391      Action = ActivityAction.Update,
 1392    });
 393
 1394    return true;
 1395  }
 396
 397  public async Task<bool> DeleteAutoTaskAsync(int autoTaskId)
 6398  {
 6399    var autoTask = await _context.AutoTasks.AsNoTracking()
 6400      .Where(t => t.Id == autoTaskId)
 6401      .FirstOrDefaultAsync()
 6402      ?? throw new LgdxNotFound404Exception();
 5403    if (autoTask.CurrentProgressId != (int)ProgressState.Template)
 4404    {
 4405      throw new LgdxValidation400Expection("AutoTaskId", "Cannot delete the task not in running status.");
 406    }
 407
 1408    _context.AutoTasks.Remove(autoTask);
 1409    await _context.SaveChangesAsync();
 410
 1411    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 1412    {
 1413      EntityName = nameof(AutoTask),
 1414      EntityId = autoTaskId.ToString(),
 1415      Action = ActivityAction.Delete,
 1416    });
 417
 1418    return true;
 1419  }
 420
 421  public async Task AbortAutoTaskAsync(int autoTaskId)
 4422  {
 4423    var autoTask = await _context.AutoTasks.AsNoTracking()
 4424      .Where(t => t.Id == autoTaskId)
 4425      .FirstOrDefaultAsync()
 4426      ?? throw new LgdxNotFound404Exception();
 427
 3428    if (autoTask.CurrentProgressId == (int)ProgressState.Template ||
 3429        autoTask.CurrentProgressId == (int)ProgressState.Completed ||
 3430        autoTask.CurrentProgressId == (int)ProgressState.Aborted)
 3431    {
 3432      throw new LgdxValidation400Expection(nameof(autoTaskId), "Cannot abort the task not in running status.");
 433    }
 434
 0435    await _activityLogService.CreateActivityLogAsync(new ActivityLogCreateBusinessModel
 0436    {
 0437      EntityName = nameof(AutoTask),
 0438      EntityId = autoTaskId.ToString(),
 0439      Action = ActivityAction.AutoTaskManualAbort,
 0440    });
 441
 0442    if (autoTask.CurrentProgressId != (int)ProgressState.Waiting &&
 0443        autoTask.AssignedRobotId != null &&
 0444        await _onlineRobotsService.SetAbortTaskAsync((Guid)autoTask.AssignedRobotId!))
 0445    {
 446      // If the robot is online, abort the task from the robot
 0447      return;
 448    }
 449    else
 0450    {
 0451      await _autoTaskSchedulerService.AutoTaskAbortApiAsync(autoTask.Id);
 0452    }
 0453  }
 454
 455  public async Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 0456  {
 0457    var result = await _autoTaskSchedulerService.AutoTaskNextApiAsync(robotId, taskId, token);
 0458    if (!result)
 0459    {
 0460      throw new LgdxValidation400Expection(nameof(token), "The next token is invalid.");
 461    }
 0462  }
 463
 464  public async Task<AutoTaskStatisticsBusinessModel> GetAutoTaskStatisticsAsync(int realmId)
 0465  {
 0466    _memoryCache.TryGetValue("Automation_Statistics", out AutoTaskStatisticsBusinessModel? autoTaskStatistics);
 0467    if (autoTaskStatistics != null)
 0468    {
 0469      return autoTaskStatistics;
 470    }
 471
 472    /*
 473     * Get queuing tasks and running tasks (total)
 474     */
 0475    DateTime CurrentDate = DateTime.UtcNow;
 0476    var waitingTaskCount = await _context.AutoTasks.AsNoTracking()
 0477      .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting)
 0478      .CountAsync();
 0479    var waitingTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0480    .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0481    .GroupBy(t => new
 0482    {
 0483      t.CreatedAt.Year,
 0484      t.CreatedAt.Month,
 0485      t.CreatedAt.Day,
 0486      t.CreatedAt.Hour
 0487    })
 0488    .Select(g => new
 0489    {
 0490      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0491      TasksCompleted = g.Count()
 0492    })
 0493    .OrderBy(g => g.Time)
 0494    .ToListAsync();
 0495    Dictionary<int, int> waitingTaskPerHourDict = [];
 0496    foreach (var taskCount in waitingTaskPerHour)
 0497    {
 0498      waitingTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0499    }
 500
 0501    var runningTaskCount = await _context.AutoTasks.AsNoTracking()
 0502      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!))
 0503      .CountAsync();
 0504    var runningTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0505    .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains((int)t.CurrentProgressId!) && t.CreatedAt > DateTime.UtcNow.Ad
 0506    .GroupBy(t => new
 0507    {
 0508      t.CreatedAt.Year,
 0509      t.CreatedAt.Month,
 0510      t.CreatedAt.Day,
 0511      t.CreatedAt.Hour
 0512    })
 0513    .Select(g => new
 0514    {
 0515      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0516      TasksCompleted = g.Count()
 0517    })
 0518    .OrderBy(g => g.Time)
 0519    .ToListAsync();
 0520    Dictionary<int, int> runningTaskPerHourDict = [];
 0521    foreach (var taskCount in runningTaskPerHour)
 0522    {
 0523      runningTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0524    }
 525
 526    /*
 527     * Get task completed / aborted in the last 24 hours
 528     */
 0529    var completedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0530    .Where(t => t.CurrentProgressId == (int)ProgressState.Completed && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0531    .GroupBy(t => new
 0532    {
 0533      t.CreatedAt.Year,
 0534      t.CreatedAt.Month,
 0535      t.CreatedAt.Day,
 0536      t.CreatedAt.Hour
 0537    })
 0538    .Select(g => new
 0539    {
 0540      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0541      TasksCompleted = g.Count()
 0542    })
 0543    .OrderBy(g => g.Time)
 0544    .ToListAsync();
 0545    Dictionary<int, int> completedTaskPerHourDict = [];
 0546    foreach (var taskCount in completedTaskPerHour)
 0547    {
 0548      completedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0549    }
 550
 0551    var abortedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0552    .Where(t => t.CurrentProgressId == (int)ProgressState.Aborted && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0553    .GroupBy(t => new
 0554    {
 0555      t.CreatedAt.Year,
 0556      t.CreatedAt.Month,
 0557      t.CreatedAt.Day,
 0558      t.CreatedAt.Hour
 0559    })
 0560    .Select(g => new
 0561    {
 0562      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0563      TasksCompleted = g.Count()
 0564    })
 0565    .OrderBy(g => g.Time)
 0566    .ToListAsync();
 0567    Dictionary<int, int> abortedTaskPerHourDict = [];
 0568    foreach (var taskCount in abortedTaskPerHour)
 0569    {
 0570      abortedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0571    }
 572
 0573    AutoTaskStatisticsBusinessModel statistics = new()
 0574    {
 0575      LastUpdatedAt = DateTime.Now,
 0576      WaitingTasks = waitingTaskCount,
 0577      RunningTasks = runningTaskCount,
 0578    };
 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
 0585    statistics.WaitingTasksTrend.Add(waitingTaskCount);
 0586    statistics.RunningTasksTrend.Add(runningTaskCount);
 0587    for (int i = 0; i < 23; i++)
 0588    {
 0589      statistics.WaitingTasksTrend.Add(waitingTaskPerHourDict.TryGetValue(i, out int count) ? count : 0);
 0590      statistics.RunningTasksTrend.Add(runningTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0591      statistics.CompletedTasksTrend.Add(completedTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0592      statistics.AbortedTasksTrend.Add(abortedTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0593    }
 0594    statistics.WaitingTasksTrend.Reverse();
 0595    statistics.RunningTasksTrend.Reverse();
 0596    statistics.CompletedTasksTrend.Reverse();
 0597    statistics.CompletedTasks = statistics.CompletedTasksTrend.Sum();
 0598    statistics.AbortedTasksTrend.Reverse();
 0599    statistics.AbortedTasks = statistics.AbortedTasksTrend.Sum();
 0600    _memoryCache.Set("Automation_Statistics", statistics, TimeSpan.FromMinutes(5));
 601
 0602    return statistics;
 0603  }
 604
 605  public async Task<AutoTaskListBusinessModel?> GetRobotCurrentTaskAsync(Guid robotId)
 0606  {
 0607    var autoTask = await _context.AutoTasks.AsNoTracking()
 0608      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!) && t.AssignedRobotId == robotId)
 0609      .Select(t => new AutoTaskListBusinessModel
 0610      {
 0611        Id = t.Id,
 0612        Name = t.Name,
 0613        Priority = t.Priority,
 0614        FlowId = t.Flow!.Id,
 0615        FlowName = t.Flow.Name,
 0616        RealmId = t.Realm.Id,
 0617        RealmName = t.Realm.Name,
 0618        AssignedRobotId = t.AssignedRobotId,
 0619        AssignedRobotName = t.AssignedRobot!.Name,
 0620        CurrentProgressId = t.CurrentProgressId,
 0621        CurrentProgressName = t.CurrentProgress.Name,
 0622      })
 0623      .FirstOrDefaultAsync();
 0624    return autoTask;
 0625  }
 626}