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
182 changes: 182 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# OdectyStat – API reference

Read-only HTTP API mikroslužby pro odečty měřidel. Určeno pro konzumaci gateway API,
které sedí před touto službou a řeší autorizaci.

- **Base URL**: `http://127.0.0.1:5080` (konfigurovatelné v `appSettings.json` přes `Kestrel:Endpoints:Http:Url`)
- **Bind**: pouze loopback – nedostupné mimo server
- **Autentizace**: žádná (autorizaci dělá gateway)
- **Write path**: tato služba zápisové endpointy **nevystavuje**, zápisy chodí výhradně přes RabbitMQ

## Konvence

- Všechny odpovědi: `application/json; charset=utf-8`, kromě `lastphoto` (binární)
- Datum/čas: ISO 8601 bez timezone, **lokální čas serveru**
- Chyby: RFC 7807 `application/problem+json`

---

## `GET /api/gauges`

Seznam všech měřidel s posledním stavem.

### Response `200 OK`

```json
[
{
"id": 1,
"description": "Vodoměr studna",
"type": "Water",
"lastValue": 123.4567,
"lastMeasurementAt": "2026-05-18T08:15:00",
"hasPhoto": true
},
{
"id": 2,
"description": "Elektroměr HDO",
"type": "Electricity",
"lastValue": 45678.0,
"lastMeasurementAt": null,
"hasPhoto": false
}
]
```

| Pole | Typ | Popis |
|---|---|---|
| `id` | `int` | ID měřidla |
| `description` | `string` | Popis (`Gauge.Description`) |
| `type` | `string` | Typ měřidla (`Gauge.Type`, např. `"Water"`, `"Electricity"`) |
| `lastValue` | `decimal` | Poslední kumulativní stav |
| `lastMeasurementAt` | `DateTime?` | Čas posledního měření; `null` pokud měřidlo nikdy nezaznamenalo měření |
| `hasPhoto` | `bool` | `true` ⇒ volání `/lastphoto` má smysl; `false` ⇒ vrátí `404` |

### Statusy

- `200` – vždy (i prázdné pole, pokud žádná měřidla nejsou)

---

## `GET /api/gauges/{id}/lastphoto`

Binární obsah fotky z posledního měření daného měřidla.

### Path parametr

| Parametr | Typ | Popis |
|---|---|---|
| `id` | `int` | ID měřidla |

### Response `200 OK`

- `Content-Type`: `image/jpeg` / `image/png` / `image/*` podle přípony (detekováno `FileExtensionContentTypeProvider`); fallback `application/octet-stream`
- `Content-Disposition`: `attachment; filename="<původní jméno souboru>"`
- Tělo: surový obsah souboru

### Statusy

- `200` – fotka existuje a vrátila se
- `404` – buď neexistuje žádný `GaugeMeasurement` s vyplněným `ImagePath` pro toto měřidlo,
nebo se soubor nepodařilo dohledat na disku (loguje se warning)
- `400` – `id` není validní int (handluje routing)

### Poznámky

Gateway by měla nejdřív zavolat `/api/gauges` a teprve když je `hasPhoto: true`,
volat `/lastphoto`. Šetří to round-trip přes 404.

Soubor se hledá pod cestou `RecognizedSuccessFolder/{id}/{yyyy-MM-dd}/{ImagePath}`,
kde datum se odvozuje z `MeasurementDateTime` ±1 den (kvůli rozdílu mezi
`MeasurementDateTime` v DB a creation-time původního souboru použitým při ukládání).

---

## `GET /health/live`

Liveness probe. Vrátí `200` vždy, dokud proces běží. Neprovádí žádné kontroly závislostí.

```json
{
"status": "Healthy",
"totalDuration": "00:00:00.0010973",
"entries": {}
}
```

### Statusy

- `200` – `status: "Healthy"`
- `503` – `status: "Unhealthy"` (v praxi nikdy, dokud proces žije)

---

## `GET /health/ready`

Readiness probe. Zkontroluje, že jsou dostupné všechny závislosti potřebné pro obsluhu requestů.

### Kontrolované závislosti

| Entry | Co se kontroluje |
|---|---|
| `sqlserver-odecty` | SQL Server (connection string `Odecty`) |
| `postgres-homeassistant` | Postgres (connection string `HomeAssistant`) |
| `postgres-diagnostics` | Postgres (connection string `Diagnostics`) |
| `rabbitmq` | AMQP connection na RabbitMQ (z `OdectySettings`) |

### Response `200 OK`

```json
{
"status": "Healthy",
"totalDuration": "00:00:00.0096718",
"entries": {
"sqlserver-odecty": {
"data": {},
"description": null,
"duration": "00:00:00.0028340",
"status": "Healthy",
"tags": ["ready"]
},
"rabbitmq": {
"data": {},
"description": null,
"duration": "00:00:00.0021110",
"status": "Healthy",
"tags": ["ready"]
}
}
}
```

### Statusy

- `200` – všechny závislosti `Healthy`
- `503` – aspoň jedna `Unhealthy` nebo `Degraded`; v `entries` je vidět která

---

## Chybové odpovědi (RFC 7807)

Neočekávané chyby vrací `application/problem+json`:

```json
{
"type": "https://tools.ietf.org/html/rfc7234#section-5.5.1",
"title": "An error occurred while processing your request.",
"status": 500,
"traceId": "00-abc...-01"
}
```

`traceId` koresponduje s OpenTelemetry trace ID, kterým si gateway může dohledat
distribuovaný trace v collectoru.

---

## Observability

- **OTLP gRPC exporter** (logs, traces, metrics) → cílí na collector běžící na localhost
- **Service name**: `OdectyStat`
- **Instrumentace**: ASP.NET Core, HttpClient, EF Core, .NET Runtime metriky
- Endpoint kolektoru řízený env var `OTEL_EXPORTER_OTLP_ENDPOINT` (default `http://localhost:4317`)
28 changes: 12 additions & 16 deletions OdectyMVC/Application/GaugeService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OdectyMVC.Contracts;
using OdectyMVC.Dto;
using OdectyMVC.Options;
Expand All @@ -18,38 +19,33 @@ public GaugeService(IGaugeContext context, IOptions<GaugeImageLocation> options)

public async Task AddNewValue(int gaugeId, decimal value, CancellationToken cancellationToken)
{
var gauge = await context.GaugeRepository.GetGauge(gaugeId, cancellationToken);
gauge.SetNewValue(value);
await context.SaveChangesAsync(cancellationToken);
await context.MessageQueue.Publish(MessageQueueRoutingKeys.GaugeMVC_Gauge_Statechanged,
new
{
GaugeId = gaugeId,
Value = value,
Datetime = DateTime.Now
}, cancellationToken);
new
{
GaugeId = gaugeId,
Value = value,
Datetime = DateTime.Now
}, cancellationToken);
}

public async Task<IEnumerable<Models.GaugeListModel>> GetGaugeList(CancellationToken cancellationToken)
{
return await context.GaugeListModelRepository.GetGaugeList(cancellationToken);
}

public async Task UpdateGaugeState(int gaugeId, decimal value, CancellationToken cancellationToken)
public async Task<IActionResult> GetLastPhoto(int gaugeId, CancellationToken cancellationToken)
{
var gauge = await context.GaugeRepository.GetGauge(gaugeId, cancellationToken);
gauge.LastValue = value;
await context.SaveChangesAsync(cancellationToken);
return await context.GaugeListModelRepository.GetLastPhoto(gaugeId, cancellationToken);
}

public async Task SaveFileForGauge(int gaugeId, MemoryStream memoryStream, CancellationToken cancellationToken)
{
var gauge = await context.GaugeRepository.GetGauge(gaugeId, cancellationToken);
var gauge = await context.GaugeListModelRepository.GetById(gaugeId, cancellationToken);
if (gauge == null)
{
throw new ArgumentException($"Gauge with id {gaugeId} not found");
}
var fileName = $"{gauge.Name}_{Guid.NewGuid():N}.jpg";
var fileName = $"{gauge.Type}_{Guid.NewGuid():N}.jpg";
memoryStream.Position = 0;
await using var stream = File.Create(string.Format(options.Value.Path, gaugeId, fileName));
await memoryStream.CopyToAsync(stream, cancellationToken);
Expand Down
7 changes: 4 additions & 3 deletions OdectyMVC/Application/IGaugeService.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using OdectyMVC.Models;
using Microsoft.AspNetCore.Mvc;
using OdectyMVC.Models;

namespace OdectyMVC.Application
{
public interface IGaugeService
{
Task AddNewValue(int gaugeId, decimal value, CancellationToken cancellationToken);
Task<IEnumerable<GaugeListModel>> GetGaugeList(CancellationToken cancellationToken);
Task UpdateGaugeState(int gaugeId, decimal value, CancellationToken cancellationToken);
Task SaveFileForGauge(int gaugeId, MemoryStream memoryStream, CancellationToken cancellationToken);
Task<IActionResult> GetLastPhoto(int gaugeId, CancellationToken cancellationToken);
}
}
}
20 changes: 0 additions & 20 deletions OdectyMVC/Business/Gauge.cs

This file was deleted.

4 changes: 1 addition & 3 deletions OdectyMVC/Contracts/IGaugeContext.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
namespace OdectyMVC.Contracts
namespace OdectyMVC.Contracts
{
public interface IGaugeContext
{
IGaugeRepository GaugeRepository { get; }
IGaugeListModelRepository GaugeListModelRepository { get; }
IMessageQueue MessageQueue { get; }
Task SaveChangesAsync(CancellationToken cancellationToken);
}
}
9 changes: 7 additions & 2 deletions OdectyMVC/Contracts/IGaugeListModelRepository.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
namespace OdectyMVC.Contracts
using Microsoft.AspNetCore.Mvc;
using OdectyMVC.Models;

namespace OdectyMVC.Contracts
{
public interface IGaugeListModelRepository
{
Task<IEnumerable<Models.GaugeListModel>> GetGaugeList(CancellationToken cancellationToken);
Task<IEnumerable<GaugeListModel>> GetGaugeList(CancellationToken cancellationToken);
Task<GaugeListModel?> GetById(int id, CancellationToken cancellationToken);
Task<IActionResult> GetLastPhoto(int id, CancellationToken cancellationToken);
}
}
9 changes: 0 additions & 9 deletions OdectyMVC/Contracts/IGaugeRepository.cs

This file was deleted.

8 changes: 7 additions & 1 deletion OdectyMVC/Controllers/GaugeController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OdectyMVC.Application;

Expand Down Expand Up @@ -26,4 +26,10 @@ public async Task<IActionResult> GaugeByImage(int id, CancellationToken cancella
await gaugeService.SaveFileForGauge(id, memoryStream, cancellationToken);
return Ok();
}

[HttpGet("{id}/lastphoto")]
public Task<IActionResult> GetLastPhoto(int id, CancellationToken cancellationToken)
{
return gaugeService.GetLastPhoto(id, cancellationToken);
}
}
18 changes: 3 additions & 15 deletions OdectyMVC/DataLayer/GaugeContext.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
using OdectyMVC.Contracts;
using OdectyMVC.Contracts;

namespace OdectyMVC.DataLayer
{
public class GaugeContext : IGaugeContext
{
private readonly GaugeDbContext context;

public GaugeContext(IGaugeRepository gaugeRepository,
public GaugeContext(
IGaugeListModelRepository gaugeListModelRepository,
IMessageQueue messageQueue,
GaugeDbContext context)
IMessageQueue messageQueue)
{
GaugeRepository = gaugeRepository;
GaugeListModelRepository = gaugeListModelRepository;
MessageQueue = messageQueue;
this.context = context;
}

public IGaugeRepository GaugeRepository { get; }
public IGaugeListModelRepository GaugeListModelRepository { get; }
public IMessageQueue MessageQueue { get; }

public Task SaveChangesAsync(CancellationToken cancellationToken)
{
context.SaveChanges();
return Task.CompletedTask;
}
}
}
25 changes: 0 additions & 25 deletions OdectyMVC/DataLayer/GaugeDbContext.cs

This file was deleted.

Loading
Loading