Files
HECO2/TestProgram/PCDashBoard/Storage/LogDb.cs
T
jeon a502322188 chore: HERV 통합 저장소 재초기화 커밋
손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋.
.claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:32:17 +09:00

202 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
using Microsoft.Data.Sqlite;
using ErvDashboard.Model;
namespace ErvDashboard.Storage
{
// 로그 시계열 실시간 저장 (SQLite 단일파일, 무제한 누적).
// - sample : 5초 1행 (시간·전원·운전/풍량·시나리오)
// - room_sample : 실별 댐퍼/센서 (sample 당 N행)
// 그래프는 LoadRecent() 로 최근 구간을 읽어 표시, 이후 실시간 append.
public sealed class LogDb : IDisposable
{
readonly SqliteConnection _conn;
public LogDb(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,
time INTEGER NOT NULL, -- DateTime.Ticks
power INTEGER, runmode INTEGER, automode INTEGER, fanmode INTEGER,
sleep INTEGER, cook INTEGER, recover INTEGER, hystpreset INTEGER);");
// 구버전 DB 마이그레이션 — 이미 있으면 무시
try { Exec("ALTER TABLE sample ADD COLUMN automode INTEGER DEFAULT 0;"); } catch { }
try { Exec("ALTER TABLE sample ADD COLUMN hystpreset INTEGER DEFAULT 0;"); } catch { }
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();
}
// 1샘플 실시간 저장 (sample + room_sample 트랜잭션 1회).
public void Insert(LogSample s)
{
using var tx = _conn.BeginTransaction();
long id;
using (var cmd = _conn.CreateCommand())
{
cmd.Transaction = tx;
cmd.CommandText =
"INSERT INTO sample(time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset) " +
"VALUES($t,$p,$rm,$am,$fm,$sl,$ck,$rc,$hp); SELECT last_insert_rowid();";
cmd.Parameters.AddWithValue("$t", s.Time.Ticks);
cmd.Parameters.AddWithValue("$p", s.Power ? 1 : 0);
cmd.Parameters.AddWithValue("$rm", (int)s.RunMode);
cmd.Parameters.AddWithValue("$am", (int)s.AutoMode);
cmd.Parameters.AddWithValue("$hp", (int)s.HystPreset);
cmd.Parameters.AddWithValue("$fm", (int)s.FanMode);
cmd.Parameters.AddWithValue("$sl", s.SmartSleep ? 1 : 0);
cmd.Parameters.AddWithValue("$ck", s.ComfortCook ? 1 : 0);
cmd.Parameters.AddWithValue("$rc", s.ReliefRecover ? 1 : 0);
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($sid,$ri,$dsa,$dra,$co2,$pm25,$pm10,$voc,$tp,$hm,$led,$lv);";
var pSid = cmd.Parameters.Add("$sid", 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);
pSid.Value = id;
for (int i = 0; i < s.Rooms.Length; i++)
{
var rm = s.Rooms[i];
pRi.Value = i;
pDsa.Value = rm.DamperSa ? 1 : 0;
pDra.Value = rm.DamperRa ? 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.Led; pLv.Value = rm.Level;
cmd.ExecuteNonQuery();
}
}
tx.Commit();
}
// 최근 max개 샘플을 시간 오름차순으로 로드 (그래프 표시용).
public List<LogSample> LoadRecent(int max)
{
using var cmd = _conn.CreateCommand();
cmd.CommandText =
"SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset FROM " +
"(SELECT * FROM sample ORDER BY id DESC LIMIT $n) ORDER BY id ASC;";
cmd.Parameters.AddWithValue("$n", max);
return Materialize(cmd);
}
// 지정 날짜(그날 00:00 ~ 다음날 00:00, 로컬)의 샘플을 시간 오름차순으로 로드.
public List<LogSample> LoadByDate(DateTime day)
{
using var cmd = _conn.CreateCommand();
cmd.CommandText =
"SELECT id,time,power,runmode,automode,fanmode,sleep,cook,recover,hystpreset " +
"FROM sample WHERE time >= $s AND time < $e ORDER BY id ASC;";
cmd.Parameters.AddWithValue("$s", day.Date.Ticks);
cmd.Parameters.AddWithValue("$e", day.Date.AddDays(1).Ticks);
return Materialize(cmd);
}
// 데이터가 존재하는 날짜 목록(오름차순) — 상단 날짜 선택 UI용.
public List<DateTime> LoadDates()
{
var list = new List<DateTime>();
using var cmd = _conn.CreateCommand();
// Ticks/일(864e9)로 버킷팅 → 날짜(자정) 환산
cmd.CommandText = "SELECT DISTINCT time/864000000000 FROM sample ORDER BY 1;";
using var rd = cmd.ExecuteReader();
while (rd.Read()) list.Add(new DateTime(rd.GetInt64(0) * TimeSpan.TicksPerDay));
return list;
}
// sample 행을 읽고 room_sample 을 붙여 LogSample 리스트로 구성 (시간 오름차순).
List<LogSample> Materialize(SqliteCommand sampleCmd)
{
var byId = new Dictionary<long, LogSample>();
var order = new List<long>();
using (var rd = sampleCmd.ExecuteReader())
{
while (rd.Read())
{
long id = rd.GetInt64(0);
byId[id] = new LogSample
{
Time = new DateTime(rd.GetInt64(1)),
Power = rd.GetInt32(2) != 0,
RunMode = (byte)rd.GetInt32(3),
AutoMode = (byte)(rd.IsDBNull(4) ? 0 : rd.GetInt32(4)),
FanMode = (byte)rd.GetInt32(5),
SmartSleep = rd.GetInt32(6) != 0,
ComfortCook = rd.GetInt32(7) != 0,
ReliefRecover = rd.GetInt32(8) != 0,
HystPreset = (byte)(rd.IsDBNull(9) ? 0 : rd.GetInt32(9)),
};
order.Add(id);
}
}
if (order.Count == 0) return new List<LogSample>();
AttachRooms(byId, order);
var result = new List<LogSample>(order.Count);
foreach (var id in order) result.Add(byId[id]);
return result;
}
// order(오름차순 id) 범위의 room_sample 을 읽어 각 LogSample.Rooms 채움.
void AttachRooms(Dictionary<long, LogSample> byId, List<long> order)
{
var rooms = new Dictionary<long, List<RoomSample>>();
using var cmd = _conn.CreateCommand();
cmd.CommandText =
"SELECT sample_id,room_idx,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", order[0]);
cmd.Parameters.AddWithValue("$max", order[order.Count - 1]);
using var rd = cmd.ExecuteReader();
while (rd.Read())
{
long sid = rd.GetInt64(0);
if (!byId.ContainsKey(sid)) continue;
if (!rooms.TryGetValue(sid, out var lst)) { lst = new List<RoomSample>(); rooms[sid] = lst; }
lst.Add(new RoomSample
{
DamperSa = rd.GetInt32(2) != 0,
DamperRa = rd.GetInt32(3) != 0,
Co2 = rd.GetInt32(4), Pm25 = rd.GetInt32(5), Pm10 = rd.GetInt32(6), Voc = rd.GetInt32(7),
Temp = rd.GetInt32(8), Humi = rd.GetInt32(9), Led = rd.GetInt32(10), Level = rd.GetInt32(11),
});
}
foreach (var id in order)
if (rooms.TryGetValue(id, out var lst)) byId[id].Rooms = lst.ToArray();
}
public void Dispose() => _conn.Dispose();
}
}