Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions Controllers/SensorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ public async Task<IActionResult> GetAllSensors(CancellationToken ct = default)
.Where(x => x.UserId == currentUserId)
.ToDictionaryAsync(x => x.SensorId, x => x.CustomName, ct);

// Fetch all voltage color thresholds for the current user
var voltageThresholds = await _context.UserSensorVoltageThresholds
.Where(x => x.UserId == currentUserId)
.ToDictionaryAsync(x => x.SensorId, x => new { x.WarningVoltage, x.CriticalVoltage }, ct);

var sensorIds = sensors.Select(s => s.Id).ToList();
var suspendedIds = await CallerSuspendedSensorIdsAsync(sensorIds, ct);

Expand All @@ -166,6 +171,11 @@ public async Task<IActionResult> GetAllSensors(CancellationToken ct = default)
var dto = _mapper.Map<SensorDto>(sensor);
if (customNames.TryGetValue(sensor.Id, out var customName))
dto.CustomName = customName;
if (voltageThresholds.TryGetValue(sensor.Id, out var threshold))
{
dto.WarningVoltage = threshold.WarningVoltage;
dto.CriticalVoltage = threshold.CriticalVoltage;
}
dto.Suspended = suspendedIds.Contains(sensor.Id);
dto.Access = IsAdmin() ? DeviceAccess.Owner : (accessById.TryGetValue(sensor.Id, out var a) ? a : DeviceAccess.Owner);
return dto;
Expand Down Expand Up @@ -214,6 +224,16 @@ public async Task<IActionResult> GetSensor(int id, CancellationToken ct = defaul
if (!string.IsNullOrEmpty(customName))
dto.CustomName = customName;

var threshold = await _context.UserSensorVoltageThresholds
.Where(x => x.UserId == currentUserId && x.SensorId == id)
.Select(x => new { x.WarningVoltage, x.CriticalVoltage })
.FirstOrDefaultAsync(ct);
if (threshold != null)
{
dto.WarningVoltage = threshold.WarningVoltage;
dto.CriticalVoltage = threshold.CriticalVoltage;
}

dto.Suspended = await IsSensorSuspendedForCallerAsync(id, ct);
dto.Access = await CallerAccessAsync(id, ct);

Expand Down Expand Up @@ -730,6 +750,9 @@ private async Task CleanUserSensorDataAsync(int sensorId, string userId)
if (customName != null)
_context.UserSensorCustomNames.Remove(customName);

_context.UserSensorVoltageThresholds.RemoveRange(
_context.UserSensorVoltageThresholds.Where(x => x.UserId == userId && x.SensorId == sensorId));

_context.SensorActivities.RemoveRange(_context.SensorActivities.Where(a => a.UserId == userId && a.SensorId == sensorId));
_context.SensorPhotos.RemoveRange(_context.SensorPhotos.Where(p => p.UserId == userId && p.SensorId == sensorId));
_context.SensorOfflineNotifications.RemoveRange(_context.SensorOfflineNotifications.Where(n => n.UserId == userId && n.SensorId == sensorId));
Expand Down Expand Up @@ -1304,6 +1327,120 @@ public async Task<IActionResult> UpdateCustomName(
return Ok(resultDto);
}

/// <summary>
/// Sets the caller's voltage color thresholds for a sensor. The reading is shown as a warning
/// below <paramref name="dto"/>.WarningVoltage and as critical below CriticalVoltage. Upserts the row.
/// </summary>
/// <param name="sensorId">The ID of the sensor.</param>
/// <param name="dto">The warning and critical voltages. Warning must be greater than critical.</param>
/// <returns>The stored thresholds.</returns>
[HttpPatch("{sensorId}/voltage-thresholds")]
[SwaggerOperation(Summary = "Sets the caller's voltage color thresholds for a sensor.")]
[SwaggerResponse(200, "Thresholds updated.", typeof(UserSensorVoltageThresholdDto))]
[SwaggerResponse(400, "Invalid thresholds.")]
[SwaggerResponse(404, "Sensor not found.")]
[SwaggerResponse(403, "User does not have access to the sensor.")]
public async Task<IActionResult> UpdateVoltageThresholds(
int sensorId,
[FromBody] UpdateVoltageThresholdDto dto)
{
_logger.LogInformation("UpdateVoltageThresholds called by {@LogData}", new { CallerUserId = User.UserId(), sensorId });

var sensor = await _context.Sensors.FindAsync(sensorId);
if (sensor == null)
{
_logger.LogWarning("UpdateVoltageThresholds not found: {@LogData}", new { sensorId });
return NotFound(new { message = "Sensor not found!" });
}

if (!await UserCanAccessSensorAsync(sensor.Id))
{
_logger.LogWarning("UpdateVoltageThresholds forbidden for {@LogData}", new { CallerUserId = User.UserId(), sensorId });
return Forbid();
}

var currentUserId = User.UserId();
if (string.IsNullOrEmpty(currentUserId))
return Unauthorized();

if (dto.WarningVoltage <= dto.CriticalVoltage)
{
_logger.LogWarning("UpdateVoltageThresholds bad request: {@LogData}", new { sensorId, CallerUserId = currentUserId });
return BadRequest(new { message = "WarningVoltage must be greater than CriticalVoltage." });
}

var entry = await _context.UserSensorVoltageThresholds
.FirstOrDefaultAsync(x => x.UserId == currentUserId && x.SensorId == sensorId);

if (entry == null)
{
entry = new UserSensorVoltageThreshold
{
UserId = currentUserId,
SensorId = sensorId,
WarningVoltage = dto.WarningVoltage,
CriticalVoltage = dto.CriticalVoltage,
CreatedAt = DateTime.UtcNow
};
_context.UserSensorVoltageThresholds.Add(entry);
}
else
{
entry.WarningVoltage = dto.WarningVoltage;
entry.CriticalVoltage = dto.CriticalVoltage;
}

await _context.SaveChangesAsync();

var resultDto = new UserSensorVoltageThresholdDto
{
UserId = entry.UserId,
SensorId = entry.SensorId,
WarningVoltage = entry.WarningVoltage,
CriticalVoltage = entry.CriticalVoltage,
CreatedAt = entry.CreatedAt
};

_logger.LogInformation("Voltage thresholds updated for {@LogData}", new { sensorId, CallerUserId = currentUserId });
return Ok(resultDto);
}

/// <summary>
/// Clears the caller's voltage color thresholds for a sensor, so the reading is no longer colored.
/// </summary>
/// <param name="sensorId">The ID of the sensor.</param>
[HttpDelete("{sensorId}/voltage-thresholds")]
[SwaggerOperation(Summary = "Clears the caller's voltage color thresholds for a sensor.")]
[SwaggerResponse(204, "Thresholds cleared.")]
[SwaggerResponse(404, "Sensor not found.")]
[SwaggerResponse(403, "User does not have access to the sensor.")]
public async Task<IActionResult> ClearVoltageThresholds(int sensorId)
{
_logger.LogInformation("ClearVoltageThresholds called by {@LogData}", new { CallerUserId = User.UserId(), sensorId });

var sensor = await _context.Sensors.FindAsync(sensorId);
if (sensor == null)
return NotFound(new { message = "Sensor not found!" });

if (!await UserCanAccessSensorAsync(sensor.Id))
return Forbid();

var currentUserId = User.UserId();
if (string.IsNullOrEmpty(currentUserId))
return Unauthorized();

var entry = await _context.UserSensorVoltageThresholds
.FirstOrDefaultAsync(x => x.UserId == currentUserId && x.SensorId == sensorId);
if (entry != null)
{
_context.UserSensorVoltageThresholds.Remove(entry);
await _context.SaveChangesAsync();
}

_logger.LogInformation("Voltage thresholds cleared for {@LogData}", new { sensorId, CallerUserId = currentUserId });
return NoContent();
}

/// <summary>
/// Retrieves the latest data point for a specific sensor.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ private async Task ClearUserOwnedRowsAsync(string userId)

_context.RefreshTokens.RemoveRange(_context.RefreshTokens.Where(t => t.UserId == userId));
_context.UserSensorCustomNames.RemoveRange(_context.UserSensorCustomNames.Where(x => x.UserId == userId));
_context.UserSensorVoltageThresholds.RemoveRange(_context.UserSensorVoltageThresholds.Where(x => x.UserId == userId));
_context.UserSwitchCustomNames.RemoveRange(_context.UserSwitchCustomNames.Where(x => x.UserId == userId));
_context.SensorActivities.RemoveRange(_context.SensorActivities.Where(a => a.UserId == userId));
_context.SensorPhotos.RemoveRange(_context.SensorPhotos.Where(p => p.UserId == userId));
Expand Down Expand Up @@ -267,6 +268,10 @@ public async Task<IActionResult> ExportData(string id)
.Where(x => x.UserId == id)
.ToDictionaryAsync(x => x.SensorId, x => x.CustomName);

var voltageThresholds = await _context.UserSensorVoltageThresholds
.Where(x => x.UserId == id)
.ToDictionaryAsync(x => x.SensorId, x => new { x.WarningVoltage, x.CriticalVoltage });

var sensors = await _context.UserSensors
.Where(us => us.UserId == id)
.Join(_context.Sensors, us => us.SensorId, s => s.Id,
Expand Down Expand Up @@ -404,6 +409,9 @@ public async Task<IActionResult> ExportData(string id)
s.Type,
DefaultName = s.DefaultName,
CustomName = sensorCustomNames.TryGetValue(s.Id, out var cn) ? cn : null,
VoltageThresholds = voltageThresholds.TryGetValue(s.Id, out var vt)
? new { vt.WarningVoltage, vt.CriticalVoltage }
: null,
Readings = sensorReadings
.Where(r => r.SensorId == s.Id)
.Select(r => new { r.Value, r.Timestamp }),
Expand Down
8 changes: 8 additions & 0 deletions Dtos/Sensor/SensorDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ public class SensorDto
public required string DefaultName { get; set; }
public required string ParentName { get; set; }

/// <summary>
/// The caller's voltage color thresholds for this sensor. Both are null when unset, in which
/// case the client leaves the reading uncolored. A reading at or above <see cref="WarningVoltage"/>
/// is normal, below it is a warning, and below <see cref="CriticalVoltage"/> is critical.
/// </summary>
public double? WarningVoltage { get; set; }
public double? CriticalVoltage { get; set; }

/// <summary>True when the caller has this owned sensor turned off / over-quota suspended. Data reads are blocked while suspended.</summary>
public bool Suspended { get; set; }

Expand Down
13 changes: 13 additions & 0 deletions Dtos/Sensor/UpdateVoltageThresholdDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;

namespace garge_api.Dtos.Sensor
{
public class UpdateVoltageThresholdDto
{
[Range(0, 100)]
public required double WarningVoltage { get; set; }

[Range(0, 100)]
public required double CriticalVoltage { get; set; }
}
}
11 changes: 11 additions & 0 deletions Dtos/Sensor/UserSensorVoltageThresholdDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace garge_api.Dtos.Sensor
{
public class UserSensorVoltageThresholdDto
{
public required string UserId { get; set; }
public required int SensorId { get; set; }
public required double WarningVoltage { get; set; }
public required double CriticalVoltage { get; set; }
public required DateTime CreatedAt { get; set; }
}
}
Loading
Loading