Files
jeon 5a96a696b1 chore: HERV 통합 저장소 초기 커밋
- 펌웨어(program), C# 대시보드(TestProgram), 시뮬레이터(Simulator),
  프로토콜/문서(Protocol, doc) 전체를 단일 저장소로 통합
- program 폴더의 별도 git 저장소를 제거하고 통합 저장소에 흡수
- 빌드 산출물(program/build, bin/obj, *.o/.elf/.bin/.hex 등) .gitignore 처리
- 사내 Synology NAS Git 원격 연결 예정

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:44:23 +09:00

115 lines
5.1 KiB
C#

using System.Net.Http.Headers;
using System.Text;
using ErvProtocol;
namespace ErvCollector.Storage
{
// InfluxDB v2 라인 프로토콜 기록 (외부 라이브러리 없이 HTTP /api/v2/write)
//
// measurement:
// erv_global,site=<site> power,run_mode,auto_state,fan_mode,sub_mode,hood,
// hyst_preset,hyst_pm25,hyst_pm10,hyst_voc,hyst_co2,error_code
// erv_room,site=<site>,room=<1..4> damper,pm25,pm10,voc,co2,air_quality,led_dim,
// load_score,final_volume
public sealed class InfluxLineWriter : IDisposable
{
readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(10) };
readonly string _writeUrl;
public event Action<string>? OnError;
public InfluxLineWriter(string url, string org, string bucket, string token)
{
_writeUrl = $"{url.TrimEnd('/')}/api/v2/write" +
$"?org={Uri.EscapeDataString(org)}&bucket={Uri.EscapeDataString(bucket)}&precision=ns";
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token);
}
public async Task WriteAsync(string site, StatusRecord s, DateTime nowUtc)
{
long ts = ToUnixNanos(nowUtc);
var sb = new StringBuilder(512);
// erv_global
sb.Append("erv_global,site=").Append(site).Append(' ')
.Append("power=").Append(s.Power).Append("i,")
.Append("run_mode=").Append(s.RunMode).Append("i,")
.Append("auto_state=").Append(s.AutoState).Append("i,")
.Append("fan_mode=").Append(s.FanMode).Append("i,")
.Append("sub_mode=").Append(s.SubMode).Append("i,")
.Append("hood=").Append(s.Hood).Append("i,")
.Append("hyst_preset=").Append(s.HystPreset).Append("i,")
.Append("hyst_pm25=").Append(s.HystPm25).Append("i,")
.Append("hyst_pm10=").Append(s.HystPm10).Append("i,")
.Append("hyst_voc=").Append(s.HystVoc).Append("i,")
.Append("hyst_co2=").Append(s.HystCo2).Append("i,")
.Append("error_code=").Append(s.ErrorCode).Append("i,")
.Append("reset=").Append(s.Reset).Append('i')
.Append(' ').Append(ts).Append('\n');
// erv_room (4실)
for (int r = 0; r < 4; r++)
{
var rm = s.Rooms[r];
sb.Append("erv_room,site=").Append(site).Append(",room=").Append(r + 1).Append(' ')
.Append("damper=").Append(rm.Damper).Append("i,")
.Append("pm25=").Append(rm.Pm25).Append("i,")
.Append("pm10=").Append(rm.Pm10).Append("i,")
.Append("voc=").Append(rm.Voc).Append("i,")
.Append("co2=").Append(rm.Co2).Append("i,")
.Append("air_quality=").Append(rm.AirQuality).Append("i,")
.Append("led_dim=").Append(rm.LedDim).Append("i,")
.Append("load_score=").Append(rm.LoadScore).Append("i,")
.Append("final_volume=").Append(rm.FinalVolume).Append('i')
.Append(' ').Append(ts).Append('\n');
}
// erv_vsp (9엔트리: 환기1~4, 바이패스, 공청1~4)
for (int i = 0; i < 9; i++)
{
var v = s.Vsp[i];
sb.Append("erv_vsp,site=").Append(site).Append(",vsp=").Append(VspInfo.Keys[i]).Append(' ')
.Append("sa=").Append(v.Sa).Append("i,")
.Append("ea=").Append(v.Ea).Append('i')
.Append(' ').Append(ts).Append('\n');
}
// erv_hyst (프리셋별 임계값: eco/normal/turbo)
string[] presetKey = { "eco", "normal", "turbo" };
for (int i = 0; i < 3; i++)
{
var h = s.HystTable[i];
sb.Append("erv_hyst,site=").Append(site).Append(",preset=").Append(presetKey[i]).Append(' ')
.Append("pm25=").Append(h.Pm25).Append("i,")
.Append("pm10=").Append(h.Pm10).Append("i,")
.Append("voc=").Append(h.Voc).Append("i,")
.Append("co2=").Append(h.Co2).Append('i')
.Append(' ').Append(ts).Append('\n');
}
try
{
using var content = new StringContent(sb.ToString(), Encoding.UTF8);
using var resp = await _http.PostAsync(_writeUrl, content);
if (!resp.IsSuccessStatusCode)
{
string body = await resp.Content.ReadAsStringAsync();
OnError?.Invoke($"Influx write HTTP {(int)resp.StatusCode}: {body}");
}
}
catch (Exception ex)
{
OnError?.Invoke($"Influx write FAIL: {ex.Message}");
}
}
static long ToUnixNanos(DateTime utc)
{
long ticks = utc.ToUniversalTime().Ticks - DateTime.UnixEpoch.Ticks; // 100ns ticks
return ticks * 100L;
}
public void Dispose() => _http.Dispose();
}
}