using System.Net.Http.Headers; using System.Text; using ErvProtocol; namespace ErvCollector.Storage { // InfluxDB v2 라인 프로토콜 기록 (외부 라이브러리 없이 HTTP /api/v2/write) // // measurement: // erv_global,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=,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? 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(); } }