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>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using ErvProtocol;
|
||||
|
||||
namespace ErvCollector.Storage
|
||||
{
|
||||
// 현장별 시계열 로그 (그래프용). InfluxDB 와 별개로 자체 SQLite 단일파일에 누적.
|
||||
// - sample : 현장 1샘플 (시간·전원·운전/자동/풍량/시나리오/프리셋)
|
||||
// - room_sample : 실별 댐퍼/센서/LED/부하
|
||||
// 그래프는 /api/history?site=&date= 로 하루치를 읽어 표시, /api/dates 로 날짜 목록.
|
||||
public sealed class HistoryDb : IDisposable
|
||||
{
|
||||
readonly SqliteConnection _conn;
|
||||
readonly object _lock = new();
|
||||
|
||||
public HistoryDb(string path)
|
||||
{
|
||||
_conn = new SqliteConnection($"Data Source={path}");
|
||||
_conn.Open();
|
||||
Exec("PRAGMA journal_mode=WAL;");
|
||||
Exec("PRAGMA synchronous=NORMAL;");
|
||||
Exec(@"CREATE TABLE IF NOT EXISTS sample(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site TEXT NOT NULL, time INTEGER NOT NULL,
|
||||
power INTEGER, runmode INTEGER, automode INTEGER, fanmode INTEGER,
|
||||
sleep INTEGER, cook INTEGER, recover INTEGER, hystpreset INTEGER);");
|
||||
Exec("CREATE INDEX IF NOT EXISTS ix_sample_site_time ON sample(site, time);");
|
||||
Exec(@"CREATE TABLE IF NOT EXISTS room_sample(
|
||||
sample_id INTEGER NOT NULL, room_idx INTEGER NOT NULL,
|
||||
damper_sa INTEGER, damper_ra INTEGER,
|
||||
co2 INTEGER, pm25 INTEGER, pm10 INTEGER, voc INTEGER,
|
||||
temp INTEGER, humi INTEGER, led INTEGER, level INTEGER,
|
||||
PRIMARY KEY(sample_id, room_idx));");
|
||||
}
|
||||
|
||||
void Exec(string sql)
|
||||
{
|
||||
using var cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// STATUS 1건 저장. time 은 로컬(날짜 버킷팅 일치).
|
||||
public void Insert(string site, StatusRecord s)
|
||||
{
|
||||
// 자동운전 세부 : 0 비자동 / 1 자동-집중 / 2 자동-분산
|
||||
int automode = s.RunMode == 2 ? (s.AutoState == 1 ? 1 : 2) : 0;
|
||||
int sleep = (s.SubMode & 0x01) != 0 ? 1 : 0;
|
||||
int cook = (s.SubMode & 0x02) != 0 ? 1 : 0;
|
||||
int recov = (s.SubMode & 0x04) != 0 ? 1 : 0;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
using var tx = _conn.BeginTransaction();
|
||||
long id;
|
||||
using (var cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText =
|
||||
"INSERT INTO sample(site,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset) " +
|
||||
"VALUES($st,$t,$p,$rm,$am,$fm,$sl,$ck,$rc,$hp); SELECT last_insert_rowid();";
|
||||
cmd.Parameters.AddWithValue("$st", site);
|
||||
cmd.Parameters.AddWithValue("$t", DateTime.Now.Ticks);
|
||||
cmd.Parameters.AddWithValue("$p", s.Power);
|
||||
cmd.Parameters.AddWithValue("$rm", s.RunMode);
|
||||
cmd.Parameters.AddWithValue("$am", automode);
|
||||
cmd.Parameters.AddWithValue("$fm", s.FanMode);
|
||||
cmd.Parameters.AddWithValue("$sl", sleep);
|
||||
cmd.Parameters.AddWithValue("$ck", cook);
|
||||
cmd.Parameters.AddWithValue("$rc", recov);
|
||||
cmd.Parameters.AddWithValue("$hp", s.HystPreset);
|
||||
id = (long)(cmd.ExecuteScalar() ?? 0L);
|
||||
}
|
||||
using (var cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText =
|
||||
"INSERT INTO room_sample(sample_id,room_idx,damper_sa,damper_ra,co2,pm25,pm10,voc,temp,humi,led,level) " +
|
||||
"VALUES($id,$ri,$dsa,$dra,$co2,$pm25,$pm10,$voc,$tp,$hm,$led,$lv);";
|
||||
var pId=cmd.Parameters.Add("$id",SqliteType.Integer); var pRi=cmd.Parameters.Add("$ri",SqliteType.Integer);
|
||||
var pDsa=cmd.Parameters.Add("$dsa",SqliteType.Integer); var pDra=cmd.Parameters.Add("$dra",SqliteType.Integer);
|
||||
var pCo2=cmd.Parameters.Add("$co2",SqliteType.Integer); var pP25=cmd.Parameters.Add("$pm25",SqliteType.Integer);
|
||||
var pP10=cmd.Parameters.Add("$pm10",SqliteType.Integer); var pVoc=cmd.Parameters.Add("$voc",SqliteType.Integer);
|
||||
var pTp=cmd.Parameters.Add("$tp",SqliteType.Integer); var pHm=cmd.Parameters.Add("$hm",SqliteType.Integer);
|
||||
var pLed=cmd.Parameters.Add("$led",SqliteType.Integer); var pLv=cmd.Parameters.Add("$lv",SqliteType.Integer);
|
||||
pId.Value = id;
|
||||
for (int i = 0; i < 4 && i < s.Rooms.Length; i++)
|
||||
{
|
||||
var rm = s.Rooms[i];
|
||||
pRi.Value=i; pDsa.Value=rm.DamperSa?1:0; pDra.Value=rm.DamperEa?1:0;
|
||||
pCo2.Value=rm.Co2; pP25.Value=rm.Pm25; pP10.Value=rm.Pm10; pVoc.Value=rm.Voc;
|
||||
pTp.Value=rm.Temp; pHm.Value=rm.Humi; pLed.Value=rm.LedDim; pLv.Value=rm.LoadScore;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
// 지정 현장·날짜(로컬 00:00~24:00) 하루치를 JSON 배열로 반환.
|
||||
public string LoadDayJson(string site, DateTime day)
|
||||
{
|
||||
long start = day.Date.Ticks, end = day.Date.AddDays(1).Ticks;
|
||||
lock (_lock)
|
||||
{
|
||||
// sample 먼저 (id 순)
|
||||
var ids = new List<long>();
|
||||
var rows = new List<string>(); // 각 sample 의 글로벌 필드 JSON 조각(rooms 채우기 전)
|
||||
var idToIdx = new Dictionary<long, int>();
|
||||
using (var cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
"SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset " +
|
||||
"FROM sample WHERE site=$s AND time>=$a AND time<$b ORDER BY id ASC;";
|
||||
cmd.Parameters.AddWithValue("$s", site);
|
||||
cmd.Parameters.AddWithValue("$a", start);
|
||||
cmd.Parameters.AddWithValue("$b", end);
|
||||
using var rd = cmd.ExecuteReader();
|
||||
while (rd.Read())
|
||||
{
|
||||
long id = rd.GetInt64(0);
|
||||
var t = new DateTime(rd.GetInt64(1));
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("{\"t\":\"").Append(t.ToString("HH:mm:ss", CultureInfo.InvariantCulture)).Append('"')
|
||||
.Append(",\"date\":\"").Append(t.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).Append('"')
|
||||
.Append(",\"power\":").Append(rd.GetInt32(2))
|
||||
.Append(",\"run_mode\":").Append(rd.GetInt32(3))
|
||||
.Append(",\"auto_mode\":").Append(rd.GetInt32(4))
|
||||
.Append(",\"fan_mode\":").Append(rd.GetInt32(5))
|
||||
.Append(",\"sleep\":").Append(rd.GetInt32(6))
|
||||
.Append(",\"cook\":").Append(rd.GetInt32(7))
|
||||
.Append(",\"recover\":").Append(rd.GetInt32(8))
|
||||
.Append(",\"hyst_preset\":").Append(rd.GetInt32(9))
|
||||
.Append(",\"rooms\":[");
|
||||
idToIdx[id] = rows.Count;
|
||||
rows.Add(sb.ToString()); // 아직 rooms 미완 — 아래서 이어붙임
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
if (ids.Count == 0) return "[]";
|
||||
|
||||
// room_sample 을 sample 별로 모음
|
||||
var roomsById = new Dictionary<long, List<string>>();
|
||||
using (var cmd = _conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
"SELECT sample_id,damper_sa,damper_ra,co2,pm25,pm10,voc,temp,humi,led,level " +
|
||||
"FROM room_sample WHERE sample_id BETWEEN $min AND $max ORDER BY sample_id, room_idx;";
|
||||
cmd.Parameters.AddWithValue("$min", ids[0]);
|
||||
cmd.Parameters.AddWithValue("$max", ids[ids.Count - 1]);
|
||||
using var rd = cmd.ExecuteReader();
|
||||
while (rd.Read())
|
||||
{
|
||||
long sid = rd.GetInt64(0);
|
||||
if (!idToIdx.ContainsKey(sid)) continue;
|
||||
if (!roomsById.TryGetValue(sid, out var lst)) { lst = new List<string>(); roomsById[sid] = lst; }
|
||||
lst.Add("{\"damper_sa\":" + rd.GetInt32(1) + ",\"damper_ra\":" + rd.GetInt32(2) +
|
||||
",\"co2\":" + rd.GetInt32(3) + ",\"pm25\":" + rd.GetInt32(4) + ",\"pm10\":" + rd.GetInt32(5) +
|
||||
",\"voc\":" + rd.GetInt32(6) + ",\"temp\":" + rd.GetInt32(7) + ",\"humi\":" + rd.GetInt32(8) +
|
||||
",\"led\":" + rd.GetInt32(9) + ",\"level\":" + rd.GetInt32(10) + "}");
|
||||
}
|
||||
}
|
||||
|
||||
var outSb = new StringBuilder("[");
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
{
|
||||
if (i > 0) outSb.Append(',');
|
||||
outSb.Append(rows[i]);
|
||||
if (roomsById.TryGetValue(ids[i], out var lst)) outSb.Append(string.Join(",", lst));
|
||||
outSb.Append("]}");
|
||||
}
|
||||
outSb.Append(']');
|
||||
return outSb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터가 있는 날짜 목록(JSON 배열, 오름차순).
|
||||
public string LoadDatesJson(string site)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var list = new List<string>();
|
||||
using var cmd = _conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT DISTINCT time/864000000000 FROM sample WHERE site=$s ORDER BY 1;";
|
||||
cmd.Parameters.AddWithValue("$s", site);
|
||||
using var rd = cmd.ExecuteReader();
|
||||
while (rd.Read())
|
||||
{
|
||||
var d = new DateTime(rd.GetInt64(0) * TimeSpan.TicksPerDay);
|
||||
list.Add("\"" + d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + "\"");
|
||||
}
|
||||
return "[" + string.Join(",", list) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _conn.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user