/* SleepLib ZEO Loader Implementation * * Copyright (c) 2019-2024 The OSCAR Team * Copyright (c) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public * License. See the file COPYING in the main directory of the source code * for more details. */ //******************************************************************************************** // Please only INCREMENT the zeo_data_version in zeo_loader.h when making changes // that change loader behaviour or modify channels in a manner that fixes old data imports. // Note that changing the data version will require a reimport of existing data for which OSCAR // does not keep a backup - so it should be avoided if possible. // i.e. there is no need to change the version when adding support for new devices //******************************************************************************************** #include #include #include "zeo_loader.h" #include "SleepLib/machine.h" #include "csv.h" ZEOLoader::ZEOLoader() { m_type = MT_SLEEPSTAGE; csv = nullptr; } ZEOLoader::~ZEOLoader() { closeCSV(); } /*15233: "Sleep Date" 15234: "ZQ" 15236: "Total Z" 15237: "Time to Z" 15237: "Time in Wake" 15238: "Time in REM" 15238: "Time in Light" 15241: "Time in Deep" 15242: "Awakenings" 15245: "Start of Night" 15246: "End of Night" 15246: "Rise Time" 15247: "Alarm Reason" 15247: "Snooze Time" 15254: "Wake Tone" 15259: "Wake Window" 15259: "Alarm Type" 15260: "First Alarm Ring" 15261: "Last Alarm Ring" 15261: "First Snooze Time" 15265: "Last Snooze Time" 15266: "Set Alarm Time" 15266: "Morning Feel" 15267: "Sleep Graph" 15267: "Detailed Sleep Graph" 15268: "Firmware Version" */ int ZEOLoader::OpenFile(const QString & filename) { if (!openCSV(filename)) { closeCSV(); return -1; } int count = 0; Session* sess; // TODO: add progress bar support, perhaps move shared logic into shared parent class with Dreem loader while ((sess = readNextSession()) != nullptr) { sess->SetChanged(true); mach->AddSession(sess); count++; } if (count > 0) { mach->Save(); mach->SaveSummaryCache(); p_profile->StoreMachines(); } closeCSV(); return count; } bool ZEOLoader::openCSV(const QString & filename) { file.setFileName(filename); if (filename.toLower().endsWith(".csv")) { if (!file.open(QFile::ReadOnly)) { qDebug() << "Couldn't open zeo file" << filename; return false; } } else {// if (filename.toLower().endsWith(".dat")) { // TODO: add direct support for .dat files return false; // not supported. } QStringList header; csv = new CSVReader(file); bool ok = csv->readRow(header); if (!ok) { qWarning() << "no header row"; return false; } csv->setFieldNames(header); MachineInfo info = newInfo(); mach = p_profile->CreateMachine(info); return true; } void ZEOLoader::closeCSV() { if (csv != nullptr) { delete csv; csv = nullptr; } if (file.isOpen()) { file.close(); } } // int idxTotalZ = header.indexOf("Total Z"); // int idxAlarmReason = header.indexOf("Alarm Reason"); // int idxSnoozeTime = header.indexOf("Snooze Time"); // int idxWakeTone = header.indexOf("Wake Tone"); // int idxWakeWindow = header.indexOf("Wake Window"); // int idxAlarmType = header.indexOf("Alarm Type"); static const EventDataType GAIN = 0.25; // allow for fractional sleep stages (such as Deep (2)) Session* ZEOLoader::readNextSession() { if (csv == nullptr) { qWarning() << "no CSV open!"; return nullptr; } Session* sess = nullptr; QDateTime start_of_night; //, end_of_night, rise_time; qint64 st, tt; int stage; int ZQ, TimeToZ, TimeInWake, TimeInREM, TimeInLight, TimeInDeep, Awakenings; int MorningFeel; //QString FirmwareVersion, MyZeoVersion; //QDateTime FirstAlarmRing, LastAlarmRing, FirstSnoozeTime, LastSnoozeTime, SetAlarmTime; QStringList /*SG,*/ DSG; QHash row; while (csv->readRow(row)) { SessionID sid = 0; invalid_fields = false; start_of_night = readDateTime(row["Start of Night"]); if (start_of_night.isValid()) { sid = start_of_night.toTime_t(); if (mach->SessionExists(sid)) { continue; } } // else invalid_fields will be true ZQ = readInt(row["ZQ"]); TimeToZ = readInt(row["Time to Z"]); TimeInWake = readInt(row["Time in Wake"]); TimeInREM = readInt(row["Time in REM"]); TimeInLight = readInt(row["Time in Light"]); TimeInDeep = readInt(row["Time in Deep"]); Awakenings = readInt(row["Awakenings"]); //end_of_night = readDateTime(row["End of Night"]); //rise_time = readDateTime(row["Rise Time"]); //FirstAlarmRing = readDateTime(row["First Alarm Ring"], false); //LastAlarmRing = readDateTime(row["Last Alarm Ring"], false); //FirstSnoozeTime = readDateTime(row["First Snooze Time"], false); //LastSnoozeTime = readDateTime(row["Last Snooze Time"], false); //SetAlarmTime = readDateTime(row["Set Alarm Time"], false); MorningFeel = readInt(row["Morning Feel"], false); //FirmwareVersion = row["Firmware Version"]; //MyZeoVersion = row["My ZEO Version"]; if (invalid_fields) { continue; } //SG = row["Sleep Graph"].trimmed().split(" "); DSG = row["Detailed Sleep Graph"].trimmed().split(" "); if (DSG.size() == 0) { continue; } sess = new Session(mach, sid); break; }; if (sess) { const int WindowSize = 30 * 1000; m_session = sess; sess->settings[ZEO_Awakenings] = Awakenings; sess->settings[ZEO_MorningFeel] = MorningFeel; sess->settings[ZEO_TimeToZ] = TimeToZ; sess->settings[ZEO_ZQ] = ZQ; sess->settings[ZEO_TimeInWake] = TimeInWake; sess->settings[ZEO_TimeInREM] = TimeInREM; sess->settings[ZEO_TimeInLight] = TimeInLight; sess->settings[ZEO_TimeInDeep] = TimeInDeep; st = qint64(start_of_night.toTime_t()) * 1000L; sess->really_set_first(st); tt = st; for (int i = 0; i < DSG.size(); i++) { bool ok; stage = DSG[i].toInt(&ok); if (ok) { // 0 = no data, 1 = Awake, 2 = REM, 3 = Light Sleep, 4 = Deep Sleep, 6 = Deep Sleep (2), drawn slightly less deep int value = -stage / GAIN; // use negative values so that the chart is oriented the right way switch (stage) { case 0: EndEventList(ZEO_SleepStage, tt); break; case 6: // According to ZeoViewer, 6 is a "Deep (2)" and is drawn somewhere between Light and Deep. value = -3.75 / GAIN; // fall through case 1: case 2: case 3: case 4: AddEvent(ZEO_SleepStage, tt, value); break; default: qWarning() << sess->session() << start_of_night << "@" << i << "unknown sleep stage" << stage; break; } } else { qWarning() << sess->session() << start_of_night << "@" << i << "unknown sleep stage" << DSG[i]; } tt += WindowSize; } EndEventList(ZEO_SleepStage, tt); sess->really_set_last(tt); //int size = DSG.size(); //qDebug() << linecomp[0] << start_of_night << end_of_night << rise_time << size << "30 second chunks"; } return sess; } void ZEOLoader::AddEvent(ChannelID channel, qint64 t, EventDataType value) { EventList* C = m_importChannels[channel]; if (C == nullptr) { C = m_session->AddEventList(channel, EVL_Event, GAIN, 0, -5, 0); Q_ASSERT(C); // Once upon a time AddEventList could return nullptr, but not any more. m_importChannels[channel] = C; } // Add the event C->AddEvent(t, value); m_importLastValue[channel] = value; } void ZEOLoader::EndEventList(ChannelID channel, qint64 t) { EventList* C = m_importChannels[channel]; if (C != nullptr) { C->AddEvent(t, m_importLastValue[channel]); // Mark this channel's event list as ended. m_importChannels[channel] = nullptr; } } QDateTime ZEOLoader::readDateTime(const QString & text, bool required) { QDateTime dt = QDateTime::fromString(text, "MM/dd/yyyy HH:mm"); if (required || !text.isEmpty()) { if (!dt.isValid()) { dt = QDateTime::fromString(text, "yyyy-MM-dd HH:mm:ss"); if (!dt.isValid()) { invalid_fields = true; } } } return dt; } int ZEOLoader::readInt(const QString & text, bool required) { bool ok; int value = text.toInt(&ok); if (!ok) { if (required) { invalid_fields = true; } else { value = 0; } } return value; } static bool zeo_initialized = false; void ZEOLoader::Register() { if (zeo_initialized) { return; } qDebug("Registering ZEOLoader"); RegisterLoader(new ZEOLoader()); //InitModelMap(); zeo_initialized = true; }