A unique way to track your sleep hours

I was looking for a way to track when I fell asleep & woke up. I looked at various apps and wearable devices but didn’t see anything that suited me well. For example, some consider you to be asleep when you are lying in bed reading a book, and others were not so good privacy-wise.

Theoretically I could just manually record the time when I go to bed and when I wake up, but that is not accurate if I have difficulty falling asleep, or if I wake up in the middle of the night, or simply forget because I am tired, etc…

I realized that checking my email is the first thing I do when I wake up, and the last thing I do before going to sleep. So, I wrote a tiny email server that my phone automatically pings whenever I check my email. That server simply records the timestamp each time. I'm using HealthPixel to record my other data such as exercise, food, and what activities I am doing, so in context of this other data, it is pretty obvious when I am awake and when I am sleeping.

I am sharing the Python source code below. Once this script is running (on an EC2 server or anywhere), you just go into your phone’s settings and add a new email account. You can actually enter any bogus username and password. The important thing is to make it only ping when you manually check your email (in iOS, choose “Manual” instead of “Fetch”/“Push”). Then you get automatic logging to a CSV file each time you check your email.

import logging
import time
import socket
import sys
import select
import pytz
from datetime import datetime

__doc__ = '''
Fake email server that writes to a CSV file each time you check your email.
Just run it on any server such as Amazon EC2 (must allow traffic on port 110),
using the filename of the script:

sudo python3 fake_email_server.py

Then add a POP3 email account on your phone using your server's IP address.
You can enter any username/password; it accepts any credentials.

adapted from http://code.activestate.com/recipes/534131-pypopper-python-pop3-server/
'''


logging.basicConfig(format="%(name)s %(levelname)s - %(message)s")
log = logging.getLogger("pypopper")
log.setLevel(logging.INFO)

CSV_PATH = 'email_checks.csv'
TIMEZONE = pytz.timezone('US/Eastern')
HOST = '0.0.0.0'  # wildcard IP address
PORT = 110  # the default POP3 port

SOCKET_LIFETIME = 15

END = "\r\n"


class ConnectionWrapper:
    '''need wrapper rather than subclass because we don't instantiate it directly'''

    _timeout = 10

    def __init__(self, conn: socket.socket):
        self.conn = conn
        # seconds for one operation until it tries again
        conn.settimeout(self._timeout)

    def sendall(self, data: str):
        if len(data) < 50:
            log.debug("send: %r", data)
        else:
            log.debug("send: %r...", data[:50])
        data += END
        self.conn.sendall(data.encode())

    def recvall(self) -> str:
        data = []
        start = time.time()
        while time.time() - start < self._timeout:
            chunk = self.conn.recv(4096).decode('utf-8')
            if END in chunk:
                data.append(chunk[: chunk.index(END)])
                break
            data.append(chunk)
            if len(data) > 1:
                pair = data[-2] + data[-1]
                if END in pair:
                    data[-2] = pair[: pair.index(END)]
                    data.pop()
                    break
        log.debug("recv: %r", "".join(data))
        return "".join(data)

    def close(self):
        return self.conn.close()

    def get_ip_address(self):
        return self.conn.getsockname()[0]


class ClientClosedSocket(Exception):
    pass


def serve(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((host, port))
    try:
        if host:
            hostname = host
        else:
            hostname = "localhost"
        log.info("POP3 serving on %s:%s", hostname, port)
        while True:
            sock.listen(1)
            inner_conn, addr = sock.accept()
            conn = ConnectionWrapper(inner_conn)

            # record the current time stamp and IP address to file
            ip_address = conn.get_ip_address()
            timestamp = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S %Z')
            with open(CSV_PATH, 'a') as f:
                f.write('{},{}\n'.format(timestamp, ip_address))

            log.debug('Connected by %s', addr)
            try:
                conn.sendall("+OK pop3 server ready")
                start = time.time()
                while time.time() - start < SOCKET_LIFETIME:
                    try:
                        _timeout_seconds = 1
                        readable, writable, in_error = select.select(
                            [inner_conn], [inner_conn], [], _timeout_seconds
                        )
                    except OSError:
                        print('OSError...socket closed from client side')
                        raise ClientClosedSocket
                    if not readable:
                        continue
                    # will raise select.error
                    data = conn.recvall()
                    if not data:
                        log.warning('Socket is supposedly readable but got no data')
                        continue
                    command = data.split(maxsplit=1)[0]
                    log.info(f'Received command: {command}')
                    response = dict(
                        USER="+OK user accepted",
                        PASS="+OK pass accepted",
                        STAT="+OK 0 0",
                        LIST="+OK\r\n.\r\n",
                        NOOP="+OK",
                        QUIT="+OK pypopper POP3 server signing off",
                    ).get(command, "-ERR unknown command")
                    conn.sendall(response)
                    if command == 'QUIT':
                        break
                log.info('Closing socket due to timeout')
            except ClientClosedSocket:
                pass
            finally:
                conn.close()
    except (SystemExit, KeyboardInterrupt):
        log.info("pypopper stopped")
    except Exception as ex:
        print('exception is', ex)
        log.critical("fatal error", exc_info=ex)
    finally:
        print('about to do sock.shutdown')
        sock.shutdown(socket.SHUT_RDWR)
        sock.close()


if __name__ == '__main__':
    serve(HOST, PORT)



Other blog posts
Tips for self-experimentation
Why a health tracking system must be flexible
Hypotheses about your health
You can't figure it all out in your head
Here is an x-ray of a banana
HealthPixel feature: Levels
The problem with the "daily diary"
How to get better results with your doctor
5 reasons why HealthPixel is better than the "daily diary"
A unique way to track your sleep hours