package com.dreikraft.axbo.data; import java.io.File; import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Combines movements records into a sleep record. * * @author jan.illetschko@3kraft.com */ public class SleepData implements Serializable { public static final long serialVersionUID = 1L; public static final Log log = LogFactory.getLog(SleepData.class); public static final long MINUTE = 60 * 1000; public static final long HOUR = 60 * 60 * 1000; public static final long SNOOZE_WAIT_INTERVAL = 2 * 60 * 1000; public static final long SNOOZE_RESTART_INTERVAL = 5 * 60 * 1000; public static final long SLEEP_TRIGGER_INTERVAL = 8 * 60 * 1000; public static final long SLEEP_START_DELAY = 4 * 60 * 1000; public static final long DEFAULT_SLEEP_DURATION = 8 * 60 * 60 * 1000; public static final String SLEEP_DATA_FILE_EXT = ".axm"; public static final String SLEEP_DATA_FILE_EXT_PATTERN = "^.*(\\.axm|\\.spw)$"; private String id; private String name; private Date wakeupTime; private Date wakeIntervalStart; private WakeInterval wakeInterval; private List<MovementData> movements; private DeviceType deviceType; private String comment; private boolean powerNap = false; private WakeType wakeType = WakeType.NONE; private String firmwareVersion; private transient Date sleepStart; private transient Date endTime; private transient File dataFile; private transient int compareStartHour; /** * Creates a new instance of SleepData */ public SleepData() { this.powerNap = false; movements = new ArrayList<>(); } /** * Creates a new instance of SleepData * * @param id * @param name * @param deviceType * @param comment */ public SleepData(final String id, final String name, final DeviceType deviceType, final String comment) { this(); this.id = id; this.name = name; this.deviceType = deviceType; this.comment = comment; } /** * Calculates the start time of this sleep record. * * @return if this is sleep record is a recorded power nap, returns the wake * interval start time, otherwise the first entry of the record. */ public Date calculateStartTime() { if (isPowerNap()) { return getWakeIntervalStart(); } else { return movements.get(0).getTimestamp(); } } /** * Calculates the hour of the day when the record starts. Used to compare * diagrams. * * @return the hour of the day */ public int calculateStartHour() { Calendar curStartCal = Calendar.getInstance(); curStartCal.setTime(calculateStartTime()); return curStartCal.get(Calendar.HOUR_OF_DAY); } /** * Calculates the end time of this sleep record. * * @return the calculated end of this sleep record */ public Date calculateEndTime() { // use previously calculated value if (endTime == null) { // calculate a default value from start time and default duration endTime = new Date(new Date().getTime() + DEFAULT_SLEEP_DURATION); if (powerNap) { // use start time plus wake interval for powernap endTime = new Date(calculateStartTime().getTime() + getWakeInterval() .getTime()); } else if (movements.size() > 0) { // otherwise take last movement entry endTime = movements.get(movements.size() - 1).getTimestamp(); } // if the record has a wake interval final Date calculatedWakeIntervalEnd = calculateWakeIntervalEnd(); endTime = calculatedWakeIntervalEnd != null ? calculatedWakeIntervalEnd : endTime; // if the record has a wake up time and its after the current calculated time if (getWakeupTime() != null && getWakeupTime().getTime() > endTime .getTime()) { endTime = getWakeupTime(); } } return endTime; } /** * Calculates the time when the person of this record felt asleep. Only * calculated once. * * @return the time when the person felt asleep */ public Date calculateSleepStart() { if (sleepStart != null) { return new Date(sleepStart.getTime()); } sleepStart = calculateStartTime(); if (powerNap) { return new Date(sleepStart.getTime()); } if (movements.size() > 0) { MovementData prevMove = movements.get(0); for (int i = 1; i < movements.size(); i++) { long delta = movements.get(i).getTimestamp().getTime() - prevMove. getTimestamp().getTime(); if (delta > SLEEP_TRIGGER_INTERVAL) { sleepStart = new Date(prevMove.getTimestamp().getTime() + SLEEP_START_DELAY); return new Date(sleepStart.getTime()); } prevMove = movements.get(i); } } return new Date(sleepStart.getTime()); } /** * Calculate the end of the wake interval. The wake interval may be extended * by using i-Snooze. * * @return the end of the wake interval or null, if there is no wake interval * in this record */ public Date calculateWakeIntervalEnd() { Date wakeIntervalEnd = null; if (getWakeIntervalStart() != null) { wakeIntervalEnd = new Date(getWakeIntervalStart().getTime() + getWakeInterval().getTime()); for (final MovementData movement : getMovements()) { if (movement.getMovementsZ() == MovementData.SNOOZE) { wakeIntervalEnd = new Date(movement.getTimestamp().getTime() + getWakeInterval().getTime()); } } } return wakeIntervalEnd; } /** * Calculates the sleep duration for this data set. If possible the wake up * time is used for the calculation. * * @return the duration of the sleep in msec */ public long calculateDuration() { if (calculateSleepStart() != null && wakeupTime != null) { return wakeupTime.getTime() - calculateSleepStart().getTime(); } else { return calculateEndTime().getTime() - calculateStartTime().getTime(); } } /** * Calculates the time between applying the sensor and falling asleep. * * @return the time in msec */ public long calculateLatency() { return calculateSleepStart().getTime() - calculateStartTime().getTime(); } /** * Calculates the sum of all movements between sleep start and wake up of this * sleep record. * * @return the movements sum */ public int calculateMovementCount() { final Date start = calculateStartTime(); final Date end = wakeupTime != null ? wakeupTime : calculateEndTime(); int count = 0; for (MovementData move : movements) { if (move.getTimestamp().after(start) && move.getTimestamp().before(end)) count += move.getMovementsX() + move.getMovementsY() + move.getMovementsZ(); } return count; } /** * Calculates the average movement count per hour. * * @return the movement count */ public double calculateMovementsPerHour() { long duration = calculateDuration(); if (duration > 0) { double durationInHours = (double) duration / (60 * 60 * 1000); return (double) calculateMovementCount() / durationInHours; } return 0; } /** * Calculates the time saving. The saving is the difference between the actual * wake up time and the latest wake up time. * * @return the timesaving in msec or zero, if it can not be calculated */ public long calculateTimeSaving() { if (wakeIntervalStart == null || wakeupTime == null) { return 0; } return Math.max(0, wakeIntervalStart.getTime() + getWakeInterval().getTime() - wakeupTime.getTime()); } /** * Finds the last movement or key press in this sleep data record. * * @return the last movement or null */ public MovementData findLastMovement() { for (int i = movements.size() - 1; i > -1; i--) { final MovementData movement = movements.get(i); if (movement.isMovement()) return movement; } return null; } /** * {@inheritDoc} */ @Override public String toString() { String s = ""; try { s = "{" + this.getClass() + ", " + this.getId() + ", " + this.getName() + ", " + this.calculateStartTime() + ", " + this.getWakeupTime() + ", " + this.getWakeIntervalStart() + ", " + this.calculateEndTime() + ", " + this.getDeviceType() + ", " + this.getComment() + ", " + this .isPowerNap() + ", " + this.getWakeType() + ", " + this. getFirmwareVersion() + "}"; } catch (Exception ex) { log.error(ex.getMessage(), ex); } return s; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getWakeupTime() { return wakeupTime != null ? new Date(wakeupTime.getTime()) : null; } public void setWakeupTime(Date wakeupTime) { this.wakeupTime = new Date(wakeupTime.getTime()); } public List<MovementData> getMovements() { return movements; } public void setMovements(List<MovementData> movements) { this.movements = movements; } public MovementData getMovement(int index) { return movements.get(index); } public void setMovements(int index, MovementData movement) { movements.set(index, movement); } public int addMovement(MovementData movement) { int index = movements.size(); movements.add(movement); return index; } public DeviceType getDeviceType() { return deviceType; } public void setDeviceType(DeviceType deviceType) { this.deviceType = deviceType; } public void setDataFile(File dataFile) { this.dataFile = dataFile; } public File getDataFile() { return dataFile; } public Date getWakeIntervalStart() { return wakeIntervalStart != null ? new Date(wakeIntervalStart.getTime()) : null; } public void setWakeIntervalStart(Date wakeIntervalStart) { this.wakeIntervalStart = new Date(wakeIntervalStart.getTime()); } public WakeInterval getWakeInterval() { if (wakeInterval == null) { wakeInterval = WakeInterval.LONG; } return wakeInterval; } public void setWakeInterval(WakeInterval wakeInterval) { this.wakeInterval = wakeInterval; } public String getComment() { return comment == null ? "" : comment; } public void setComment(String comment) { this.comment = comment; } public boolean isPowerNap() { return powerNap; } public void setPowerNap(boolean powerNap) { this.powerNap = powerNap; } public WakeType getWakeType() { return wakeType; } public void setWakeType(WakeType wakeType) { this.wakeType = wakeType; } public int getCompareStartHour() { return compareStartHour; } public void setCompareStartHour(int compareStartHour) { this.compareStartHour = compareStartHour; } public String getFirmwareVersion() { return firmwareVersion; } public void setFirmwareVersion(String firmwareVersion) { this.firmwareVersion = firmwareVersion; } public String getSleepDataFilename() { return getName().replaceAll(" ", "_") + "_" + new SimpleDateFormat( "yyyy_MM_dd_HH_mm").format(new Date( calculateStartTime().getTime())) + SLEEP_DATA_FILE_EXT; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final SleepData other = (SleepData) obj; final Date thisSleepStart = calculateStartTime(); final Date otherSleepStart = other.calculateStartTime(); if (!thisSleepStart.equals(otherSleepStart)) { return false; } final long thisDuration = calculateDuration(); final long otherDuration = other.calculateDuration(); if (thisDuration != otherDuration) { return false; } final int thisMovementCount = calculateMovementCount(); final int otherMovementCount = other.calculateMovementCount(); if (thisMovementCount != otherMovementCount) { return false; } return true; } @Override public int hashCode() { int hash = 7; hash = 83 * hash + calculateSleepStart().hashCode(); hash = 83 * hash + (int) calculateDuration(); hash = 83 * hash + calculateMovementCount(); return hash; } }