@@ -59,13 +59,146 @@ public double standardDeviationCalculator(List<Integer> data, double median) {
5959 return sd ;
6060 }
6161
62- public Duration estimateDuration (String topic , ZonedDateTime start , ZonedDateTime end ) {
62+ public String estimateDuration (String topic , ZonedDateTime start , ZonedDateTime end ) {
6363 List <SleepData > sessionData = mqttDataRepository .findByTopicAndTimestampBetween (topic , start , end );
6464 if (sessionData .isEmpty ()) {
65- return Duration .ZERO ;
65+ return Duration .ZERO . toString () ;
6666 }
6767 sessionData .sort (Comparator .comparing (SleepData ::getTimestamp ));
68+ final Duration maxGap = Duration .ofMinutes (30 );
6869 Duration totalSleep = Duration .ZERO ;
69- // TODO...
70+
71+ ZonedDateTime segmentStart = null ; // start of a contiguous in-bed segment
72+ ZonedDateTime prevTs = null ; // previous sample timestamp
73+
74+ for (SleepData s : sessionData ) {
75+ final ZonedDateTime ts = s .getTimestamp ();
76+ final int inBed = s .getInBed ();
77+
78+ // If there is a large gap, close any open segment at the previous timestamp
79+ if (prevTs != null && ts .isAfter (prevTs .plus (maxGap )) && segmentStart != null ) {
80+ totalSleep = totalSleep .plus (Duration .between (segmentStart , prevTs ));
81+ segmentStart = null ;
82+ }
83+
84+
85+ if (inBed == 1 ) {
86+ // Open a segment if not already open
87+ if (segmentStart == null ) {
88+ segmentStart = ts ;
89+ }
90+ } else { // inBed == 0
91+ // Close an active segment at the last known timestamp (prevTs) to avoid over-counting
92+ if (segmentStart != null ) {
93+ ZonedDateTime endTs = (prevTs != null && !prevTs .isBefore (segmentStart )) ? prevTs : ts ;
94+ totalSleep = totalSleep .plus (Duration .between (segmentStart , endTs ));
95+ segmentStart = null ;
96+ }
97+ }
98+
99+ prevTs = ts ;
100+ }
101+
102+ // If the last samples are still in-bed, close at the last timestamp we saw
103+ if (segmentStart != null && prevTs != null ) {
104+ totalSleep = totalSleep .plus (Duration .between (segmentStart , prevTs ));
105+ }
106+
107+ return totalSleep .isNegative () ? Duration .ZERO .toString () : totalSleep .toString ();
108+ }
109+
110+ /*
111+ * return JSON with
112+ * - Event (turnoverNumber,
113+ * largeBodyMove,
114+ * minorBodyMove,
115+ * apneaEvents,
116+ * )
117+ * - Time stamp
118+ */
119+ public String timeSeries (String topic , ZonedDateTime start , ZonedDateTime end ) {
120+ List <SleepData > sessionData = mqttDataRepository .findByTopicAndTimestampBetween (topic , start , end );
121+ if (sessionData == null || sessionData .isEmpty ()) {
122+ return "[]" ;
123+ }
124+
125+ sessionData .sort (Comparator .comparing (SleepData ::getTimestamp ));
126+
127+ List <java .util .Map <String , Object >> payload = new java .util .ArrayList <>(sessionData .size ());
128+
129+ for (SleepData s : sessionData ) {
130+ java .util .Map <String , Object > event = new java .util .LinkedHashMap <>();
131+ event .put ("turnoverNumber" , s .getTurnoverNumber ());
132+ event .put ("largeBodyMove" , s .getLargeBodyMove ());
133+ event .put ("minorBodyMove" , s .getMinorBodyMove ());
134+ event .put ("apneaEvents" , s .getApneaEvents ());
135+
136+ java .util .Map <String , Object > point = new java .util .LinkedHashMap <>();
137+ point .put ("event" , event );
138+ point .put ("timestamp" , s .getTimestamp ());
139+
140+ payload .add (point );
141+ }
142+
143+ // Serialize using Jackson (ISO-8601 for ZonedDateTime)
144+ com .fasterxml .jackson .databind .ObjectMapper mapper = new com .fasterxml .jackson .databind .ObjectMapper ();
145+ mapper .registerModule (new com .fasterxml .jackson .datatype .jsr310 .JavaTimeModule ());
146+ mapper .disable (com .fasterxml .jackson .databind .SerializationFeature .WRITE_DATES_AS_TIMESTAMPS );
147+
148+ try {
149+ return mapper .writeValueAsString (payload );
150+ } catch (com .fasterxml .jackson .core .JsonProcessingException e ) {
151+ return "[]" ;
152+ }
153+ }
154+
155+ /*
156+ * return JSON with
157+ * - New sleepState
158+ * - Time stamp
159+ */
160+ public String sleepStateSeries (String topic , ZonedDateTime start , ZonedDateTime end ) {
161+ List <SleepData > sessionData = mqttDataRepository .findByTopicAndTimestampBetween (topic , start , end );
162+ if (sessionData == null || sessionData .isEmpty ()) {
163+ return "[]" ;
164+ }
165+
166+ // Sort chronologically
167+ sessionData .sort (Comparator .comparing (SleepData ::getTimestamp ));
168+
169+ List <java .util .Map <String , Object >> changes = new java .util .ArrayList <>();
170+
171+ Integer prev = null ;
172+ for (SleepData s : sessionData ) {
173+ Integer curr = s .getSleepState ();
174+ if (curr == null ) {
175+ continue ; // skip malformed samples
176+ }
177+ if (prev == null ) {
178+ prev = curr ; // initialize baseline without emitting an event
179+ continue ;
180+ }
181+ if (!curr .equals (prev )) {
182+ java .util .Map <String , Object > change = new java .util .LinkedHashMap <>();
183+ change .put ("NewState" , curr );
184+ change .put ("TimeStamp" , s .getTimestamp ());
185+ changes .add (change );
186+ prev = curr ;
187+ }
188+ }
189+
190+ com .fasterxml .jackson .databind .ObjectMapper mapper = new com .fasterxml .jackson .databind .ObjectMapper ();
191+ mapper .registerModule (new com .fasterxml .jackson .datatype .jsr310 .JavaTimeModule ());
192+ mapper .disable (com .fasterxml .jackson .databind .SerializationFeature .WRITE_DATES_AS_TIMESTAMPS );
193+
194+ try {
195+ return mapper .writeValueAsString (changes );
196+ } catch (com .fasterxml .jackson .core .JsonProcessingException e ) {
197+ return "[]" ;
198+ }
199+ }
200+
201+ public ZonedDateTime getTimeDelta () {
202+ return ZonedDateTime .now ().minusDays (1 ).withHour (16 ).withMinute (0 ).withSecond (0 ).withNano (0 );
70203 }
71204}
0 commit comments