< 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: 314
Uncovered lines: 144
Coverable lines: 458
Total lines: 563
Line coverage: 68.5%
Branch coverage
68%
Covered branches: 62
Total branches: 90
Branch coverage: 68.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%1010100%
GetAutoTasksAsync()100%1212100%
GetAutoTaskAsync()100%22100%
ValidateAutoTask()100%1212100%
CreateAutoTaskAsync()83.33%66100%
UpdateAutoTaskAsync()80%101096.42%
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.Common;
 3using LGDXRobotCloud.API.Services.Navigation;
 4using LGDXRobotCloud.Data.DbContexts;
 5using LGDXRobotCloud.Data.Entities;
 6using LGDXRobotCloud.Data.Models.Business.Automation;
 7using LGDXRobotCloud.Data.Models.Business.Navigation;
 8using LGDXRobotCloud.Utilities.Enums;
 9using LGDXRobotCloud.Utilities.Helpers;
 10using MassTransit;
 11using Microsoft.EntityFrameworkCore;
 12using Microsoft.Extensions.Caching.Memory;
 13
 14namespace LGDXRobotCloud.API.Services.Automation;
 15
 16public interface IAutoTaskService
 17{
 18  Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? name, AutoTas
 19  Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId);
 20  Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel);
 21  Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel);
 22  Task<bool> DeleteAutoTaskAsync(int autoTaskId);
 23
 24  Task AbortAutoTaskAsync(int autoTaskId);
 25  Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token);
 26
 27  Task<AutoTaskStatisticsBusinessModel> GetAutoTaskStatisticsAsync(int realmId);
 28  Task<AutoTaskListBusinessModel?> GetRobotCurrentTaskAsync(Guid robotId);
 29}
 30
 3531public class AutoTaskService(
 3532    LgdxContext context,
 3533    IAutoTaskSchedulerService autoTaskSchedulerService,
 3534    IBus bus,
 3535    IEventService eventService,
 3536    IMemoryCache memoryCache,
 3537    IOnlineRobotsService onlineRobotsService
 3538  ) : IAutoTaskService
 39{
 3540  private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na
 3541  private readonly IMemoryCache _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
 3542  private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context));
 3543  private readonly IAutoTaskSchedulerService _autoTaskSchedulerService = autoTaskSchedulerService ?? throw new ArgumentN
 3544  private readonly IBus _bus = bus ?? throw new ArgumentNullException(nameof(bus));
 45
 46  public async Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? 
 947  {
 948    var query = _context.AutoTasks as IQueryable<AutoTask>;
 949    if (!string.IsNullOrWhiteSpace(name))
 350    {
 351      name = name.Trim();
 352      query = query.Where(t => t.Name != null && t.Name.ToLower().Contains(name.ToLower()));
 353    }
 954    if (realmId != null)
 955    {
 956      query = query.Where(t => t.RealmId == realmId);
 957    }
 958    switch (autoTaskCatrgory)
 59    {
 60      case AutoTaskCatrgory.Template:
 161        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Template);
 162        break;
 63      case AutoTaskCatrgory.Waiting:
 164        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Waiting);
 165        break;
 66      case AutoTaskCatrgory.Completed:
 167        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Completed);
 168        break;
 69      case AutoTaskCatrgory.Aborted:
 170        query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Aborted);
 171        break;
 72      case AutoTaskCatrgory.Running:
 173        query = query.Where(t => t.CurrentProgressId > (int)ProgressState.Aborted);
 174        break;
 75    }
 976    var itemCount = await query.CountAsync();
 977    var PaginationHelper = new PaginationHelper(itemCount, pageNumber, pageSize);
 978    var autoTasks = await query.AsNoTracking()
 979      .OrderByDescending(t => t.Priority)
 980      .ThenBy(t => t.Id)
 981      .Skip(pageSize * (pageNumber - 1))
 982      .Take(pageSize)
 983      .Select(t => new AutoTaskListBusinessModel
 984      {
 985        Id = t.Id,
 986        Name = t.Name,
 987        Priority = t.Priority,
 988        FlowId = t.Flow!.Id,
 989        FlowName = t.Flow.Name,
 990        RealmId = t.Realm.Id,
 991        RealmName = t.Realm.Name,
 992        AssignedRobotId = t.AssignedRobotId,
 993        AssignedRobotName = t.AssignedRobot!.Name,
 994        CurrentProgressId = t.CurrentProgressId,
 995        CurrentProgressName = t.CurrentProgress.Name,
 996      })
 997      .AsSplitQuery()
 998      .ToListAsync();
 999    return (autoTasks, PaginationHelper);
 9100  }
 101
 102  public async Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId)
 2103  {
 2104    return await _context.AutoTasks.AsNoTracking()
 2105      .Where(t => t.Id == autoTaskId)
 2106      .Include(t => t.AutoTaskDetails
 2107        .OrderBy(td => td.Order))
 2108        .ThenInclude(td => td.Waypoint)
 2109      .Include(t => t.AutoTaskJourneys)
 2110        .ThenInclude(tj => tj.CurrentProgress)
 2111      .Include(t => t.AssignedRobot)
 2112      .Include(t => t.CurrentProgress)
 2113      .Include(t => t.Flow)
 2114      .Include(t => t.Realm)
 2115      .AsSplitQuery()
 2116      .Select(t => new AutoTaskBusinessModel
 2117      {
 2118        Id = t.Id,
 2119        Name = t.Name,
 2120        Priority = t.Priority,
 2121        FlowId = t.Flow!.Id,
 2122        FlowName = t.Flow.Name,
 2123        RealmId = t.Realm.Id,
 2124        RealmName = t.Realm.Name,
 2125        AssignedRobotId = t.AssignedRobotId,
 2126        AssignedRobotName = t.AssignedRobot!.Name,
 2127        CurrentProgressId = t.CurrentProgressId,
 2128        CurrentProgressName = t.CurrentProgress.Name,
 2129        AutoTaskJourneys = t.AutoTaskJourneys.Select(tj => new AutoTaskJourneyBusinessModel
 2130        {
 2131          Id = tj.Id,
 2132          CurrentProcessId = tj.CurrentProgressId,
 2133          CurrentProcessName = tj.CurrentProgress == null ? null : tj.CurrentProgress.Name,
 2134          CreatedAt = tj.CreatedAt,
 2135        })
 2136        .OrderBy(tj => tj.Id)
 2137        .ToList(),
 2138        AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel
 2139        {
 2140          Id = td.Id,
 2141          Order = td.Order,
 2142          CustomX = td.CustomX,
 2143          CustomY = td.CustomY,
 2144          CustomRotation = td.CustomRotation,
 2145          Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel
 2146          {
 2147            Id = td.Waypoint.Id,
 2148            Name = td.Waypoint.Name,
 2149            RealmId = t.Realm.Id,
 2150            RealmName = t.Realm.Name,
 2151            X = td.Waypoint.X,
 2152            Y = td.Waypoint.Y,
 2153            Rotation = td.Waypoint.Rotation,
 2154            IsParking = td.Waypoint.IsParking,
 2155            HasCharger = td.Waypoint.HasCharger,
 2156            IsReserved = td.Waypoint.IsReserved,
 2157          },
 2158        })
 2159        .OrderBy(td => td.Order)
 2160        .ToList()
 2161      })
 2162      .FirstOrDefaultAsync()
 2163        ?? throw new LgdxNotFound404Exception();
 1164  }
 165
 166  private async Task ValidateAutoTask(HashSet<int> waypointIds, int flowId, int realmId, Guid? robotId)
 7167  {
 168    // Validate the Waypoint IDs
 7169    var waypointDict = await _context.Waypoints.AsNoTracking()
 7170      .Where(w => waypointIds.Contains(w.Id))
 7171      .Where(w => w.RealmId == realmId)
 11172      .ToDictionaryAsync(w => w.Id, w => w);
 26173    foreach (var waypointId in waypointIds)
 3174    {
 3175      if (!waypointDict.ContainsKey(waypointId))
 1176      {
 1177        throw new LgdxValidation400Expection(nameof(Waypoint), $"The Waypoint ID {waypointId} is invalid.");
 178      }
 2179    }
 180    // Validate the Flow ID
 6181    var flow = await _context.Flows.Where(f => f.Id == flowId).AnyAsync();
 6182    if (flow == false)
 1183    {
 1184      throw new LgdxValidation400Expection(nameof(Flow), $"The Flow ID {flowId} is invalid.");
 185    }
 186    // Validate the Realm ID
 5187    var realm = await _context.Realms.Where(r => r.Id == realmId).AnyAsync();
 5188    if (realm == false)
 1189    {
 1190      throw new LgdxValidation400Expection(nameof(Realm), $"The Realm ID {realmId} is invalid.");
 191    }
 192    // Validate the Assigned Robot ID
 4193    if (robotId != null)
 4194    {
 4195      var robot = await _context
 4196        .Robots
 4197        .Where(r => r.Id == robotId)
 4198        .Where(r => r.RealmId == realmId)
 4199        .AnyAsync();
 4200      if (robot == false)
 1201      {
 1202        throw new LgdxValidation400Expection(nameof(Robot), $"Robot ID: {robotId} is invalid.");
 203      }
 3204    }
 3205  }
 206
 207  public async Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel)
 6208  {
 6209    HashSet<int> waypointIds = autoTaskCreateBusinessModel.AutoTaskDetails
 3210      .Where(d => d.WaypointId != null)
 3211      .Select(d => d.WaypointId!.Value)
 6212      .ToHashSet();
 213
 6214    await ValidateAutoTask(waypointIds,
 6215      autoTaskCreateBusinessModel.FlowId,
 6216      autoTaskCreateBusinessModel.RealmId,
 6217      autoTaskCreateBusinessModel.AssignedRobotId);
 218
 2219    var autoTask = new AutoTask
 2220    {
 2221      Name = autoTaskCreateBusinessModel.Name,
 2222      Priority = autoTaskCreateBusinessModel.Priority,
 2223      FlowId = autoTaskCreateBusinessModel.FlowId,
 2224      RealmId = autoTaskCreateBusinessModel.RealmId,
 2225      AssignedRobotId = autoTaskCreateBusinessModel.AssignedRobotId,
 2226      CurrentProgressId = autoTaskCreateBusinessModel.IsTemplate
 2227        ? (int)ProgressState.Template
 2228        : (int)ProgressState.Waiting,
 2229      AutoTaskDetails = autoTaskCreateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail
 2230      {
 2231        Order = td.Order,
 2232        CustomX = td.CustomX,
 2233        CustomY = td.CustomY,
 2234        CustomRotation = td.CustomRotation,
 2235        WaypointId = td.WaypointId,
 2236      })
 0237      .OrderBy(td => td.Order)
 2238      .ToList(),
 2239    };
 2240    await _context.AutoTasks.AddAsync(autoTask);
 2241    await _context.SaveChangesAsync();
 2242    _autoTaskSchedulerService.ResetIgnoreRobot(autoTask.RealmId);
 2243    eventService.AutoTaskHasCreated();
 244
 2245    var autoTaskBusinessModel = await _context.AutoTasks.AsNoTracking()
 2246      .Where(t => t.Id == autoTask.Id)
 2247      .Select(t => new AutoTaskBusinessModel
 2248      {
 2249        Id = t.Id,
 2250        Name = t.Name,
 2251        Priority = t.Priority,
 2252        FlowId = t.Flow!.Id,
 2253        FlowName = t.Flow.Name,
 2254        RealmId = t.Realm.Id,
 2255        RealmName = t.Realm.Name,
 2256        AssignedRobotId = t.AssignedRobotId,
 2257        AssignedRobotName = t.AssignedRobot!.Name,
 2258        CurrentProgressId = t.CurrentProgressId,
 2259        CurrentProgressName = t.CurrentProgress.Name,
 2260        AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel
 2261        {
 2262          Id = td.Id,
 2263          Order = td.Order,
 2264          CustomX = td.CustomX,
 2265          CustomY = td.CustomY,
 2266          CustomRotation = td.CustomRotation,
 2267          Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel
 2268          {
 2269            Id = td.Waypoint.Id,
 2270            Name = td.Waypoint.Name,
 2271            RealmId = t.Realm.Id,
 2272            RealmName = t.Realm.Name,
 2273            X = td.Waypoint.X,
 2274            Y = td.Waypoint.Y,
 2275            Rotation = td.Waypoint.Rotation,
 2276            IsParking = td.Waypoint.IsParking,
 2277            HasCharger = td.Waypoint.HasCharger,
 2278            IsReserved = td.Waypoint.IsReserved,
 2279          },
 2280        })
 2281        .OrderBy(td => td.Order)
 2282        .ToList()
 2283      })
 2284      .FirstOrDefaultAsync()
 2285        ?? throw new LgdxNotFound404Exception();
 286
 2287    if (autoTask.CurrentProgressId == (int)ProgressState.Waiting)
 1288    {
 1289      var autoTaskJourney = new AutoTaskJourney
 1290      {
 1291        AutoTaskId = autoTaskBusinessModel.Id,
 1292        CurrentProgressId = autoTask.CurrentProgressId
 1293      };
 1294      await _context.AutoTasksJourney.AddAsync(autoTaskJourney);
 1295      await _context.SaveChangesAsync();
 296
 1297      await _bus.Publish(autoTaskBusinessModel.ToContract());
 1298    }
 299
 2300    return autoTaskBusinessModel;
 2301  }
 302
 303  public async Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel)
 4304  {
 4305    var task = await _context.AutoTasks
 4306      .Where(t => t.Id == autoTaskId)
 4307      .Include(t => t.AutoTaskDetails
 4308        .OrderBy(td => td.Order))
 4309      .FirstOrDefaultAsync()
 4310      ?? throw new LgdxNotFound404Exception();
 311
 3312    if (task.CurrentProgressId != (int)ProgressState.Template)
 1313    {
 1314      throw new LgdxValidation400Expection("AutoTaskId", "Only AutoTask Templates are editable.");
 315    }
 316    // Ensure the input task does not include Detail ID from other task
 6317    HashSet<int> dbDetailIds = task.AutoTaskDetails.Select(d => d.Id).ToHashSet();
 9318    foreach (var bmDetailId in autoTaskUpdateBusinessModel.AutoTaskDetails.Where(d => d.Id != null).Select(d => d.Id))
 1319    {
 1320      if (bmDetailId != null && !dbDetailIds.Contains((int)bmDetailId))
 1321      {
 1322        throw new LgdxValidation400Expection("AutoTaskDetailId", $"The Task Detail ID {(int)bmDetailId} is belongs to ot
 323      }
 0324    }
 1325    HashSet<int> waypointIds = autoTaskUpdateBusinessModel.AutoTaskDetails
 0326      .Where(d => d.WaypointId != null)
 0327      .Select(d => d.WaypointId!.Value)
 1328      .ToHashSet();
 1329    await ValidateAutoTask(waypointIds,
 1330      autoTaskUpdateBusinessModel.FlowId,
 1331      task.RealmId,
 1332      autoTaskUpdateBusinessModel.AssignedRobotId);
 333
 1334    task.Name = autoTaskUpdateBusinessModel.Name;
 1335    task.Priority = autoTaskUpdateBusinessModel.Priority;
 1336    task.FlowId = autoTaskUpdateBusinessModel.FlowId;
 1337    task.AssignedRobotId = autoTaskUpdateBusinessModel.AssignedRobotId;
 1338    task.AutoTaskDetails = autoTaskUpdateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail
 1339    {
 1340      Id = (int)td.Id!,
 1341      Order = td.Order,
 1342      CustomX = td.CustomX,
 1343      CustomY = td.CustomY,
 1344      CustomRotation = td.CustomRotation,
 1345      WaypointId = td.WaypointId,
 1346    }).ToList();
 1347    await _context.SaveChangesAsync();
 1348    return true;
 1349  }
 350
 351  public async Task<bool> DeleteAutoTaskAsync(int autoTaskId)
 6352  {
 6353    var autoTask = await _context.AutoTasks.AsNoTracking()
 6354      .Where(t => t.Id == autoTaskId)
 6355      .FirstOrDefaultAsync()
 6356      ?? throw new LgdxNotFound404Exception();
 5357    if (autoTask.CurrentProgressId != (int)ProgressState.Template)
 4358    {
 4359      throw new LgdxValidation400Expection("AutoTaskId", "Cannot delete the task not in running status.");
 360    }
 361
 1362    _context.AutoTasks.Remove(autoTask);
 1363    await _context.SaveChangesAsync();
 364
 1365    return true;
 1366  }
 367
 368  public async Task AbortAutoTaskAsync(int autoTaskId)
 6369  {
 6370    var autoTask = await _context.AutoTasks.AsNoTracking()
 6371      .Where(t => t.Id == autoTaskId)
 6372      .FirstOrDefaultAsync()
 6373      ?? throw new LgdxNotFound404Exception();
 374
 5375    if (autoTask.CurrentProgressId == (int)ProgressState.Template ||
 5376        autoTask.CurrentProgressId == (int)ProgressState.Completed ||
 5377        autoTask.CurrentProgressId == (int)ProgressState.Aborted)
 3378    {
 3379      throw new LgdxValidation400Expection(nameof(autoTaskId), "Cannot abort the task not in running status.");
 380    }
 2381    if (autoTask.CurrentProgressId != (int)ProgressState.Waiting &&
 2382        autoTask.AssignedRobotId != null &&
 2383        await _onlineRobotsService.SetAbortTaskAsync((Guid)autoTask.AssignedRobotId!, true))
 1384    {
 385      // If the robot is online, abort the task from the robot
 1386      return;
 387    }
 388    else
 1389    {
 1390      await _autoTaskSchedulerService.AutoTaskAbortApiAsync(autoTask.Id);
 1391    }
 2392  }
 393
 394  public async Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token)
 2395  {
 2396    var result = await _autoTaskSchedulerService.AutoTaskNextApiAsync(robotId, taskId, token)
 2397      ?? throw new LgdxValidation400Expection(nameof(token), "The next token is invalid.");
 1398    _onlineRobotsService.SetAutoTaskNextApi(robotId, result);
 1399  }
 400
 401  public async Task<AutoTaskStatisticsBusinessModel> GetAutoTaskStatisticsAsync(int realmId)
 0402  {
 0403    _memoryCache.TryGetValue("Automation_Statistics", out AutoTaskStatisticsBusinessModel? autoTaskStatistics);
 0404    if (autoTaskStatistics != null)
 0405    {
 0406      return autoTaskStatistics;
 407    }
 408
 409    /*
 410     * Get queuing tasks and running tasks (total)
 411     */
 0412    DateTime CurrentDate = DateTime.UtcNow;
 0413    var waitingTaskCount = await _context.AutoTasks.AsNoTracking()
 0414      .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting)
 0415      .CountAsync();
 0416    var waitingTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0417    .Where(t => t.CurrentProgressId == (int)ProgressState.Waiting && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0418    .GroupBy(t => new
 0419    {
 0420      t.CreatedAt.Year,
 0421      t.CreatedAt.Month,
 0422      t.CreatedAt.Day,
 0423      t.CreatedAt.Hour
 0424    })
 0425    .Select(g => new
 0426    {
 0427      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0428      TasksCompleted = g.Count()
 0429    })
 0430    .OrderBy(g => g.Time)
 0431    .ToListAsync();
 0432    Dictionary<int, int> waitingTaskPerHourDict = [];
 0433    foreach (var taskCount in waitingTaskPerHour)
 0434    {
 0435      waitingTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0436    }
 437
 0438    var runningTaskCount = await _context.AutoTasks.AsNoTracking()
 0439      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!))
 0440      .CountAsync();
 0441    var runningTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0442    .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains((int)t.CurrentProgressId!) && t.CreatedAt > DateTime.UtcNow.Ad
 0443    .GroupBy(t => new
 0444    {
 0445      t.CreatedAt.Year,
 0446      t.CreatedAt.Month,
 0447      t.CreatedAt.Day,
 0448      t.CreatedAt.Hour
 0449    })
 0450    .Select(g => new
 0451    {
 0452      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0453      TasksCompleted = g.Count()
 0454    })
 0455    .OrderBy(g => g.Time)
 0456    .ToListAsync();
 0457    Dictionary<int, int> runningTaskPerHourDict = [];
 0458    foreach (var taskCount in runningTaskPerHour)
 0459    {
 0460      runningTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0461    }
 462
 463    /*
 464     * Get task completed / aborted in the last 24 hours
 465     */
 0466    var completedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0467    .Where(t => t.CurrentProgressId == (int)ProgressState.Completed && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0468    .GroupBy(t => new
 0469    {
 0470      t.CreatedAt.Year,
 0471      t.CreatedAt.Month,
 0472      t.CreatedAt.Day,
 0473      t.CreatedAt.Hour
 0474    })
 0475    .Select(g => new
 0476    {
 0477      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0478      TasksCompleted = g.Count()
 0479    })
 0480    .OrderBy(g => g.Time)
 0481    .ToListAsync();
 0482    Dictionary<int, int> completedTaskPerHourDict = [];
 0483    foreach (var taskCount in completedTaskPerHour)
 0484    {
 0485      completedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0486    }
 487
 0488    var abortedTaskPerHour = await _context.AutoTasksJourney.AsNoTracking()
 0489    .Where(t => t.CurrentProgressId == (int)ProgressState.Aborted && t.CreatedAt > DateTime.UtcNow.AddHours(-23))
 0490    .GroupBy(t => new
 0491    {
 0492      t.CreatedAt.Year,
 0493      t.CreatedAt.Month,
 0494      t.CreatedAt.Day,
 0495      t.CreatedAt.Hour
 0496    })
 0497    .Select(g => new
 0498    {
 0499      Time = new DateTime(g.Key.Year, g.Key.Month, g.Key.Day, g.Key.Hour, 0, 0),
 0500      TasksCompleted = g.Count()
 0501    })
 0502    .OrderBy(g => g.Time)
 0503    .ToListAsync();
 0504    Dictionary<int, int> abortedTaskPerHourDict = [];
 0505    foreach (var taskCount in abortedTaskPerHour)
 0506    {
 0507      abortedTaskPerHourDict.Add((int)(CurrentDate - taskCount.Time).TotalHours, taskCount.TasksCompleted);
 0508    }
 509
 0510    AutoTaskStatisticsBusinessModel statistics = new()
 0511    {
 0512      LastUpdatedAt = DateTime.Now,
 0513      WaitingTasks = waitingTaskCount,
 0514      RunningTasks = runningTaskCount,
 0515    };
 516
 517    /*
 518     * Get trends
 519     * Note that the completed tasks and aborted tasks are looked up in the last 24 hours
 520     */
 521    // Now = total tasks
 0522    statistics.WaitingTasksTrend.Add(waitingTaskCount);
 0523    statistics.RunningTasksTrend.Add(runningTaskCount);
 0524    for (int i = 0; i < 23; i++)
 0525    {
 0526      statistics.WaitingTasksTrend.Add(waitingTaskPerHourDict.TryGetValue(i, out int count) ? count : 0);
 0527      statistics.RunningTasksTrend.Add(runningTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0528      statistics.CompletedTasksTrend.Add(completedTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0529      statistics.AbortedTasksTrend.Add(abortedTaskPerHourDict.TryGetValue(i, out count) ? count : 0);
 0530    }
 0531    statistics.WaitingTasksTrend.Reverse();
 0532    statistics.RunningTasksTrend.Reverse();
 0533    statistics.CompletedTasksTrend.Reverse();
 0534    statistics.CompletedTasks = statistics.CompletedTasksTrend.Sum();
 0535    statistics.AbortedTasksTrend.Reverse();
 0536    statistics.AbortedTasks = statistics.AbortedTasksTrend.Sum();
 0537    _memoryCache.Set("Automation_Statistics", statistics, TimeSpan.FromMinutes(5));
 538
 0539    return statistics;
 0540  }
 541
 542  public async Task<AutoTaskListBusinessModel?> GetRobotCurrentTaskAsync(Guid robotId)
 0543  {
 0544    var autoTask = await _context.AutoTasks.AsNoTracking()
 0545      .Where(t => !LgdxHelper.AutoTaskStaticStates.Contains(t.CurrentProgressId!) && t.AssignedRobotId == robotId)
 0546      .Select(t => new AutoTaskListBusinessModel
 0547      {
 0548        Id = t.Id,
 0549        Name = t.Name,
 0550        Priority = t.Priority,
 0551        FlowId = t.Flow!.Id,
 0552        FlowName = t.Flow.Name,
 0553        RealmId = t.Realm.Id,
 0554        RealmName = t.Realm.Name,
 0555        AssignedRobotId = t.AssignedRobotId,
 0556        AssignedRobotName = t.AssignedRobot!.Name,
 0557        CurrentProgressId = t.CurrentProgressId,
 0558        CurrentProgressName = t.CurrentProgress.Name,
 0559      })
 0560      .FirstOrDefaultAsync();
 0561    return autoTask;
 0562  }
 563}