Compare commits
2 Commits
postgres-m
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 77a7e60ab4 | |||
| f60e85c6a8 |
@@ -1,196 +1,196 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class TelemetryController : ControllerBase
|
||||
{
|
||||
private readonly ITelemetryService telemetryService;
|
||||
private readonly IAlertService alertService;
|
||||
private readonly IAlertConditionService alertConditionService;
|
||||
|
||||
public TelemetryController(ITelemetryService telemetryService, IAlertService alertService, IAlertConditionService alertConditionService)
|
||||
{
|
||||
this.telemetryService = telemetryService;
|
||||
this.alertService = alertService;
|
||||
this.alertConditionService = alertConditionService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<TelemetryDto>>> List([FromQuery] int? deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] DateTime? sinceUtc, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new TelemetryFilter { DeviceId = deviceId, StartDateUtc = startUtc, EndDateUtc = endUtc, SinceUtc = sinceUtc, Page = page, PageSize = pageSize };
|
||||
var result = await telemetryService.ListAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("AddData")]
|
||||
public async Task<ActionResult<string>> Create([FromQuery] TelemetryDto telementry, [FromQuery] string? lastUpdateDate, CancellationToken cancellationToken)
|
||||
{
|
||||
telementry.TimestampUtc = DateTime.UtcNow;
|
||||
telementry.RelayStates = ParseRelayStates(Request.Query);
|
||||
var id = await telemetryService.AddAsync(telementry, cancellationToken);
|
||||
|
||||
var parts = new List<string> { $"ID:{id}" };
|
||||
var deviceId = telementry.DeviceId;
|
||||
|
||||
if (deviceId > 0)
|
||||
{
|
||||
// 1. Check alerts and power status
|
||||
var alertResult = await alertService.CheckAndSendAlertsAsync(deviceId, telementry, cancellationToken);
|
||||
if (alertResult != null)
|
||||
{
|
||||
if (alertResult.Messages.Count > 0)
|
||||
{
|
||||
parts.Add($"A:{alertResult.Messages.Count}");
|
||||
parts.Add($"R:{alertResult.Recipients}");
|
||||
parts.Add($"M:{string.Join("@", alertResult.Messages)}");
|
||||
}
|
||||
|
||||
if (alertResult.PowerStatus != null)
|
||||
{
|
||||
parts.Add($"P:{alertResult.PowerStatus}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check relay conditions update
|
||||
if (!string.IsNullOrEmpty(lastUpdateDate))
|
||||
{
|
||||
var parsedDate = ParseLastUpdateDate(lastUpdateDate);
|
||||
var relayConditions = await alertConditionService.GetUpdatedRelayConditionsAsync(deviceId, parsedDate, cancellationToken);
|
||||
if (relayConditions.Count > 0)
|
||||
{
|
||||
parts.Add("RU:1");
|
||||
parts.Add($"RL:{relayConditions.Count}");
|
||||
parts.Add($"RC:{EncodeRelayConditions(relayConditions)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(string.Join("|", parts));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode relay conditions into a compact string format for STM32 parsing.
|
||||
/// Format (relays separated by |):
|
||||
/// E{enabled}R{relayIndex}{groups}
|
||||
/// Group (OR between groups):
|
||||
/// G{action} where action = 0 (turn on) | 1 (turn off)
|
||||
/// followed by its conditions joined by & (AND inside a group):
|
||||
/// Sensor condition: S{sensor}O{op}V{value1}[,{value2}]
|
||||
/// Time condition: T{start}-{end} (minutes from midnight)
|
||||
/// Time ranges with start > end cross midnight (e.g. T1200-360 = 20:00..06:00);
|
||||
/// the device must treat that as (now >= start || now < end).
|
||||
/// </summary>
|
||||
private static string EncodeRelayConditions(IReadOnlyList<RelayConditionDto> conditions)
|
||||
{
|
||||
return string.Join("|", conditions.Select(c =>
|
||||
{
|
||||
var groups = new List<string>();
|
||||
groups.AddRange(c.On.Select(g => EncodeGroup(g, 0)));
|
||||
groups.AddRange(c.Off.Select(g => EncodeGroup(g, 1)));
|
||||
return $"E{c.Enabled}R{c.RelayIndex}{string.Concat(groups)}";
|
||||
}));
|
||||
}
|
||||
|
||||
private static string EncodeGroup(RelayGroupDto group, int action)
|
||||
{
|
||||
return $"G{action}{string.Join("&", group.Conditions.Select(EncodeCondition))}";
|
||||
}
|
||||
|
||||
private static string EncodeCondition(RelayRuleDto condition)
|
||||
{
|
||||
if (condition.Kind == 1)
|
||||
{
|
||||
return $"T{condition.Start ?? 0}-{condition.End ?? 0}";
|
||||
}
|
||||
|
||||
var encoded = $"S{condition.Sensor}O{condition.Op}V{condition.V1}";
|
||||
if (condition.V2.HasValue)
|
||||
encoded += $",{condition.V2.Value}";
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect relay states from query params r1, r2, ... (missing relay = off/0).
|
||||
/// </summary>
|
||||
private static string? ParseRelayStates(IQueryCollection query)
|
||||
{
|
||||
var entries = new List<(int Index, string Value)>();
|
||||
foreach (var key in query.Keys)
|
||||
{
|
||||
var match = Regex.Match(key, @"^r(\d+)$", RegexOptions.IgnoreCase);
|
||||
if (!match.Success || !int.TryParse(match.Groups[1].Value, out var index) || index <= 0)
|
||||
continue;
|
||||
|
||||
entries.Add((index, query[key].ToString() == "1" ? "1" : "0"));
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
return null;
|
||||
|
||||
var states = new string[entries.Max(e => e.Index)];
|
||||
Array.Fill(states, "0");
|
||||
foreach (var entry in entries)
|
||||
states[entry.Index - 1] = entry.Value;
|
||||
|
||||
return string.Join(",", states);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse date string in format: yyyy/MM/dd-HH:mm
|
||||
/// </summary>
|
||||
private static DateTime? ParseLastUpdateDate(string dateStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dateStr))
|
||||
return null;
|
||||
|
||||
// Try parsing as yyyy/MM/dd-HH:mm
|
||||
if (DateTime.TryParseExact(dateStr, "yyyy/MM/dd-HH:mm", null, System.Globalization.DateTimeStyles.None, out var result))
|
||||
{
|
||||
return DateTime.SpecifyKind(result, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
// Try parsing as yyyy-MM-dd HH:mm (ISO-like)
|
||||
if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out var result2))
|
||||
{
|
||||
return DateTime.SpecifyKind(result2, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpGet("minmax")]
|
||||
public async Task<ActionResult<TelemetryMinMax>> MinMax([FromQuery] int deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await telemetryService.GetMinMaxAsync(deviceId, startUtc, endUtc, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("days")]
|
||||
public async Task<ActionResult<IReadOnlyList<DayCount>>> Days([FromQuery] int deviceId, [FromQuery] int year, [FromQuery] int month, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await telemetryService.GetMonthDaysAsync(deviceId, year, month, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("months")]
|
||||
public async Task<ActionResult<IReadOnlyList<int>>> Months([FromQuery] int deviceId, [FromQuery] int year, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await telemetryService.GetActiveMonthsAsync(deviceId, year, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("aggregate")]
|
||||
public async Task<ActionResult<IReadOnlyList<TelemetryBucketDto>>> Aggregate([FromQuery] int deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] int bucketMinutes = 15, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await telemetryService.GetAggregatedAsync(deviceId, startUtc, endUtc, bucketMinutes, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using GreenHome.Application;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GreenHome.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class TelemetryController : ControllerBase
|
||||
{
|
||||
private readonly ITelemetryService telemetryService;
|
||||
private readonly IAlertService alertService;
|
||||
private readonly IAlertConditionService alertConditionService;
|
||||
|
||||
public TelemetryController(ITelemetryService telemetryService, IAlertService alertService, IAlertConditionService alertConditionService)
|
||||
{
|
||||
this.telemetryService = telemetryService;
|
||||
this.alertService = alertService;
|
||||
this.alertConditionService = alertConditionService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<TelemetryDto>>> List([FromQuery] int? deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] DateTime? sinceUtc, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = new TelemetryFilter { DeviceId = deviceId, StartDateUtc = startUtc, EndDateUtc = endUtc, SinceUtc = sinceUtc, Page = page, PageSize = pageSize };
|
||||
var result = await telemetryService.ListAsync(filter, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("AddData")]
|
||||
public async Task<ActionResult<string>> Create([FromQuery] TelemetryDto telementry, [FromQuery] string? lastUpdateDate, CancellationToken cancellationToken)
|
||||
{
|
||||
telementry.TimestampUtc = DateTime.UtcNow;
|
||||
telementry.RelayStates = ParseRelayStates(Request.Query);
|
||||
var id = await telemetryService.AddAsync(telementry, cancellationToken);
|
||||
|
||||
var parts = new List<string> { $"ID:{id}" };
|
||||
var deviceId = telementry.DeviceId;
|
||||
|
||||
if (deviceId > 0)
|
||||
{
|
||||
// 1. Check alerts and power status
|
||||
var alertResult = await alertService.CheckAndSendAlertsAsync(deviceId, telementry, cancellationToken);
|
||||
if (alertResult != null)
|
||||
{
|
||||
if (alertResult.Messages.Count > 0)
|
||||
{
|
||||
parts.Add($"A:{alertResult.Messages.Count}");
|
||||
parts.Add($"R:{alertResult.Recipients}");
|
||||
parts.Add($"M:{string.Join("@", alertResult.Messages)}");
|
||||
}
|
||||
|
||||
if (alertResult.PowerStatus != null)
|
||||
{
|
||||
parts.Add($"P:{alertResult.PowerStatus}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check relay conditions update
|
||||
if (!string.IsNullOrEmpty(lastUpdateDate))
|
||||
{
|
||||
var parsedDate = ParseLastUpdateDate(lastUpdateDate);
|
||||
var relayConditions = await alertConditionService.GetUpdatedRelayConditionsAsync(deviceId, parsedDate, cancellationToken);
|
||||
if (relayConditions.Count > 0)
|
||||
{
|
||||
parts.Add("RU:1");
|
||||
parts.Add($"RL:{relayConditions.Count}");
|
||||
parts.Add($"RC:{EncodeRelayConditions(relayConditions)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(string.Join("|", parts));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode relay conditions into a compact string format for STM32 parsing.
|
||||
/// Format (relays separated by |):
|
||||
/// E{enabled}R{relayIndex}{groups}
|
||||
/// Group (OR between groups):
|
||||
/// G{action} where action = 0 (turn on) | 1 (turn off)
|
||||
/// followed by its conditions joined by & (AND inside a group):
|
||||
/// Sensor condition: S{sensor}O{op}V{value1}[,{value2}]
|
||||
/// Time condition: T{start}-{end} (minutes from midnight)
|
||||
/// Time ranges with start > end cross midnight (e.g. T1200-360 = 20:00..06:00);
|
||||
/// the device must treat that as (now >= start || now < end).
|
||||
/// </summary>
|
||||
private static string EncodeRelayConditions(IReadOnlyList<RelayConditionDto> conditions)
|
||||
{
|
||||
return string.Join("|", conditions.Select(c =>
|
||||
{
|
||||
var groups = new List<string>();
|
||||
groups.AddRange(c.On.Select(g => EncodeGroup(g, 0)));
|
||||
groups.AddRange(c.Off.Select(g => EncodeGroup(g, 1)));
|
||||
return $"E{c.Enabled}R{c.RelayIndex}{string.Concat(groups)}";
|
||||
}));
|
||||
}
|
||||
|
||||
private static string EncodeGroup(RelayGroupDto group, int action)
|
||||
{
|
||||
return $"G{action}{string.Join("&", group.Conditions.Select(EncodeCondition))}";
|
||||
}
|
||||
|
||||
private static string EncodeCondition(RelayRuleDto condition)
|
||||
{
|
||||
if (condition.Kind == 1)
|
||||
{
|
||||
return $"T{condition.Start ?? 0}-{condition.End ?? 0}";
|
||||
}
|
||||
|
||||
var encoded = $"S{condition.Sensor}O{condition.Op}V{condition.V1}";
|
||||
if (condition.V2.HasValue)
|
||||
encoded += $",{condition.V2.Value}";
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect relay states from query params r1, r2, ... (missing relay = off/0).
|
||||
/// </summary>
|
||||
private static string? ParseRelayStates(IQueryCollection query)
|
||||
{
|
||||
var entries = new List<(int Index, string Value)>();
|
||||
foreach (var key in query.Keys)
|
||||
{
|
||||
var match = Regex.Match(key, @"^r(\d+)$", RegexOptions.IgnoreCase);
|
||||
if (!match.Success || !int.TryParse(match.Groups[1].Value, out var index) || index <= 0)
|
||||
continue;
|
||||
|
||||
entries.Add((index, query[key].ToString() == "1" ? "1" : "0"));
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
return null;
|
||||
|
||||
var states = new string[entries.Max(e => e.Index)];
|
||||
Array.Fill(states, "0");
|
||||
foreach (var entry in entries)
|
||||
states[entry.Index - 1] = entry.Value;
|
||||
|
||||
return string.Join(",", states);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse date string in format: yyyy/MM/dd-HH:mm
|
||||
/// </summary>
|
||||
private static DateTime? ParseLastUpdateDate(string dateStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dateStr))
|
||||
return null;
|
||||
|
||||
// Try parsing as yyyy/MM/dd-HH:mm
|
||||
if (DateTime.TryParseExact(dateStr, "yyyy/MM/dd-HH:mm", null, System.Globalization.DateTimeStyles.None, out var result))
|
||||
{
|
||||
return DateTime.SpecifyKind(result, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
// Try parsing as yyyy-MM-dd HH:mm (ISO-like)
|
||||
if (DateTime.TryParseExact(dateStr, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out var result2))
|
||||
{
|
||||
return DateTime.SpecifyKind(result2, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpGet("minmax")]
|
||||
public async Task<ActionResult<TelemetryMinMax>> MinMax([FromQuery] int deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await telemetryService.GetMinMaxAsync(deviceId, startUtc, endUtc, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("days")]
|
||||
public async Task<ActionResult<IReadOnlyList<DayCount>>> Days([FromQuery] int deviceId, [FromQuery] int year, [FromQuery] int month, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await telemetryService.GetMonthDaysAsync(deviceId, year, month, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("months")]
|
||||
public async Task<ActionResult<IReadOnlyList<int>>> Months([FromQuery] int deviceId, [FromQuery] int year, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await telemetryService.GetActiveMonthsAsync(deviceId, year, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("aggregate")]
|
||||
public async Task<ActionResult<IReadOnlyList<TelemetryBucketDto>>> Aggregate([FromQuery] int deviceId, [FromQuery] DateTime? startUtc, [FromQuery] DateTime? endUtc, [FromQuery] int bucketMinutes = 15, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await telemetryService.GetAggregatedAsync(deviceId, startUtc, endUtc, bucketMinutes, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user