Пример #1
0
    def _update_weather(self):
        try:
            with open('/root/weather-cache.pickle', 'rb') as wf:
                saved_weather = pickle.load(wf)
                self.forecast_time = saved_weather['then']
                response = saved_weather['forecast']
                self.forecast = Forecast(response.json(), response,
                                         response.headers)
                logging.debug("Loaded weather data from cache")
        except Exception as e:
            print "No cached weather data, or failed to load cache"
            print e
            self.forecast_time = datetime(1980, 1, 1)

        if self.weather:
            since = datetime.now() - self.forecast_time
            if since.total_seconds() >= (60 * 60):
                logging.debug(
                    "Weather cache is %d seconds old; fetching new data" %
                    since.total_seconds())
                try:
                    self.forecast = forecastio.load_forecast(
                        self.darksky_key, self.latitude, self.longitude)
                except Exception as e:
                    print "Failed to load forecast data"
                    print e
                    return

                self.forecast_time = datetime.now()
                with open('/root/weather-cache.pickle', 'wb') as wf:
                    pickle.dump(
                        {
                            'then': self.forecast_time,
                            'forecast': self.forecast.response
                        }, wf)
Пример #2
0
def get_forecast(requestURL, t=0):
    forecastio_reponse = requests.get(requestURL, timeout=t)

    json = forecastio_reponse.json()
    headers = forecastio_reponse.headers

    return Forecast(json, forecastio_reponse, headers)
Пример #3
0
def get_forecast(requestURL):
    forecastio_reponse = requests.get(requestURL)
    forecastio_reponse.raise_for_status()

    json = forecastio_reponse.json()
    headers = forecastio_reponse.headers

    return Forecast(json, forecastio_reponse, headers)
Пример #4
0
    def _request_forecast(self):
        forecastio_response = requests.get(self.request_url)
        forecastio_response.raise_for_status()

        return Forecast(forecastio_response.json(), forecastio_response,
                        forecastio_response.headers)
Пример #5
0
def make_forecast(response):
    return Forecast(response.json(), response, response.headers)
Пример #6
0
class AlarmClock():

    weather_icons = {
        'clear-day': unichr(0xf00d),
        'clear-night': unichr(0xf02e),
        'rain': unichr(0xf019),
        'snow': unichr(0xf076),
        'sleet': unichr(0xf0b5),
        'wind': unichr(0xf050),
        'fog': unichr(0xf014),
        'cloudy': unichr(0xf013),
        'partly-cloudy-day': unichr(0xf002),
        'partly-cloudy-night': unichr(0xf086),
        'hail': unichr(0xf015),
        'thunderstorm': unichr(0xf01d),
        'tornado': unichr(0xf056)
    }

    def __init__(self, epd):

        self.epd = epd
        self.menu_font = '/usr/share/fonts/truetype/freefont/FreeSans.ttf'

        # TODO: Move these out into a separate file
        self.possible_fonts = [{
            'filename':
            '/usr/share/fonts/truetype/fonts-georgewilliams/CaslonBold.ttf',
            'title': 'Caslon Bold'
        }, {
            'filename':
            '/usr/share/fonts/truetype/fonts-georgewilliams/Caslon-Black.ttf',
            'title': 'Caslon Black'
        }, {
            'filename':
            '/usr/share/fonts/truetype/fonts-georgewilliams/Caliban.ttf',
            'title': 'Caliban'
        }, {
            'filename':
            '/usr/share/fonts/truetype/fonts-georgewilliams/Cupola.ttf',
            'title': 'Cupola'
        }, {
            'filename': '/usr/share/fonts/truetype/freefont/FreeSerif.ttf',
            'title': 'Serif'
        }, {
            'filename': '/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf',
            'title': 'Serif Bold'
        }, {
            'filename': '/usr/share/fonts/truetype/freefont/FreeMono.ttf',
            'title': 'Monospace'
        }, {
            'filename': '/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf',
            'title': 'Monospace Bold'
        }, {
            'filename':
            '/usr/share/fonts/truetype/freefont/FreeMonoOblique.ttf',
            'title': 'Monospace Oblique'
        }, {
            'filename':
            '/usr/share/fonts/truetype/freefont/FreeMonoBoldOblique.ttf',
            'title': 'Monospace Bold Oblique'
        }, {
            'filename': '/usr/share/fonts/truetype/freefont/FreeSans.ttf',
            'title': 'Sans-Serif'
        }, {
            'filename': '/usr/share/fonts/truetype/freefont/FreeSansBold.ttf',
            'title': 'Sans-Serif Bold'
        }, {
            'filename':
            '/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf',
            'title': 'Sans-Serif Oblique'
        }, {
            'filename':
            '/usr/share/fonts/truetype/freefont/FreeSansBoldOblique.ttf',
            'title': 'Sans-Serif Bold Oblique'
        }, {
            'filename': '/usr/share/fonts/truetype/humor-sans/Humor-Sans.ttf',
            'title': 'Humor'
        }, {
            'filename': '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf',
            'title': 'DejaVu'
        }, {
            'filename':
            '/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf',
            'title': 'DejaVu Bold'
        }, {
            'filename': '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',
            'title': 'DejaVu Monospace'
        }, {
            'filename':
            '/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf',
            'title': 'DejaVu Monospace Bold'
        }, {
            'filename': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
            'title': 'DejaVu Sans'
        }, {
            'filename': '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
            'title': 'DejaVu Sans Bold'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/Dustismo.ttf',
            'title': 'Dustismo'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/dustismo_bold.ttf',
            'title': 'Dustismo Bold'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/dustismo_italic.ttf',
            'title': 'Dustismo Italic'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/Dustismo_Roman.ttf',
            'title': 'Dustismo Roman'
        }, {
            'filename':
            '/usr/share/fonts/truetype/dustin/Dustismo_Roman_Bold.ttf',
            'title': 'Dustismo Roman Bold'
        }, {
            'filename':
            '/usr/share/fonts/truetype/dustin/Dustismo_Roman_Italic.ttf',
            'title': 'Dustismo Roman Italic'
        }, {
            'filename':
            '/usr/share/fonts/truetype/dustin/Domestic_Manners.ttf',
            'title': 'Domestic Manners'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/Junkyard.ttf',
            'title': 'Junkyard'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/Wargames.ttf',
            'title': 'Wargames'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/PenguinAttack.ttf',
            'title': 'Penguin Attack'
        }, {
            'filename': '/usr/share/fonts/truetype/dustin/It_wasn_t_me.ttf',
            'title': 'It wasn\'t me!'
        }]

        self.possible_tones = [
            {
                'filename': '/root/tones/Blues.mp3',
                'title': 'Blues'
            },
            {
                'filename': '/root/tones/Piano Riff.mp3',
                'title': 'Piano Riff'
            },
            {
                'filename': '/root/tones/Sci-Fi.mp3',
                'title': 'Sci-Fi'
            },
            {
                'filename': '/root/tones/Pinball.mp3',
                'title': 'Pinball'
            },
            {
                'filename': '/root/tones/Crickets.mp3',
                'title': 'Crickets'
            },
            {
                'filename': '/root/tones/Motorcycle.mp3',
                'title': 'Motorcycle'
            },
            {
                'filename': '/root/tones/Timba.mp3',
                'title': 'Timba'
            },
            {
                'filename': '/root/tones/Bark.mp3',
                'title': 'Bark'
            },
            {
                'filename': '/root/tones/Trill.mp3',
                'title': 'Trill'
            },
            {
                'filename': '/root/tones/Robot.mp3',
                'title': 'Robot'
            },
            {
                'filename': '/root/tones/Old Phone.mp3',
                'title': 'Old Phone'
            },
            {
                'filename': '/root/tones/Marimba.mp3',
                'title': 'Marimba'
            },
            {
                'filename': '/root/tones/Boing.mp3',
                'title': 'Boing'
            },
            {
                'filename': '/root/tones/Strum.mp3',
                'title': 'Strum'
            },
            {
                'filename': '/root/tones/Xylophone.mp3',
                'title': 'Xylophone'
            },
            {
                'filename': '/root/tones/Digital.mp3',
                'title': 'Digital'
            },
            {
                'filename': '/root/tones/Time Passing.mp3',
                'title': 'Time Passing'
            },
            {
                'filename': '/root/tones/Harp.mp3',
                'title': 'Harp'
            },
            {
                'filename': '/root/tones/Bell Tower.mp3',
                'title': 'Bell Tower'
            },
            {
                'filename': '/root/tones/Alarm.mp3',
                'title': 'Alarm'
            },
            {
                'filename': '/root/tones/Old Car Horn.mp3',
                'title': 'Old Car Horn'
            },
            {
                'filename': '/root/tones/Doorbell.mp3',
                'title': 'Doorbell'
            },
            {
                'filename': '/root/tones/Sonar.mp3',
                'title': 'Sonar'
            },
            {
                'filename': '/root/tones/Ascending.mp3',
                'title': 'Ascending'
            },
            {
                'filename': '/root/tones/Duck.mp3',
                'title': 'Duck'
            },
        ]

        try:
            with open("/sys/class/gpio/unexport", "w") as unexport:
                unexport.write("133\n")
        except IOError:
            pass

        GPIO.setup("CSID1", GPIO.OUT)
        GPIO.output("CSID1", GPIO.LOW)

        self.mode = "weather"
        self.timezones = country_timezones('US')
        self.timezone_index = 0
        self.stop_alarming()
        atexit.register(self.stop_alarming)
        self.snooze = 0

        self.lock = threading.Lock()  # create lock for rotary switch
        self.pips = 0
        self.presses = 0

        self.re = RotaryEncoder([0x11], invert=False, factor=0.5)
        self.re.register_callbacks(turn=self.turn_cb, press=self.press_cb)

        try:
            ip = json.load(urlopen('http://jsonip.com'))['ip']
        except Exception:
            try:
                ip = json.load(
                    urlopen('https://api.ipify.org/?format=json'))['ip']
            except Exception:
                raise

        g = GeoIP.open("/usr/share/GeoIP/GeoIPCity.dat", GeoIP.GEOIP_STANDARD)
        try:
            gr = g.record_by_addr(ip)
            print gr
            self.latitude = gr['latitude']
            self.longitude = gr['longitude']
            while self.timezones[self.timezone_index] != gr['time_zone']:
                self.timezone_index += 1
        except Exception:
            raise

        try:
            with open('/root/alarmclock-settings.pickle', 'rb') as settings:
                self.settings = pickle.load(settings)
        except Exception as e:
            print "Failed to load settings; using defaults"
            print e
            self.settings = {}
            self.settings['twentyfour'] = False
            self.settings['alarm'] = False
            self.settings['font_index'] = 21
            self.settings['tone_index'] = 0
            self.settings['alarm_time'] = 360  # Minutes from midnight

        try:
            with open('/root/darksky.key', 'r') as f:
                self.darksky_key = f.readline().strip()
                self.weather = True
        except Exception as e:
            print "Couldn't get key from /root/darksky.key: " + str(e)
            self.weather = False

        if self.weather:
            self._update_weather()

    def _set_setting(self, setting, value):
        self.settings[setting] = value
        with open('/root/alarmclock-settings.pickle', 'wb') as settings:
            pickle.dump(self.settings, settings)

    def _update_weather(self):
        try:
            with open('/root/weather-cache.pickle', 'rb') as wf:
                saved_weather = pickle.load(wf)
                self.forecast_time = saved_weather['then']
                response = saved_weather['forecast']
                self.forecast = Forecast(response.json(), response,
                                         response.headers)
                logging.debug("Loaded weather data from cache")
        except Exception as e:
            print "No cached weather data, or failed to load cache"
            print e
            self.forecast_time = datetime(1980, 1, 1)

        if self.weather:
            since = datetime.now() - self.forecast_time
            if since.total_seconds() >= (60 * 60):
                logging.debug(
                    "Weather cache is %d seconds old; fetching new data" %
                    since.total_seconds())
                try:
                    self.forecast = forecastio.load_forecast(
                        self.darksky_key, self.latitude, self.longitude)
                except Exception as e:
                    print "Failed to load forecast data"
                    print e
                    return

                self.forecast_time = datetime.now()
                with open('/root/weather-cache.pickle', 'wb') as wf:
                    pickle.dump(
                        {
                            'then': self.forecast_time,
                            'forecast': self.forecast.response
                        }, wf)
        #else:
        # Weather is disabled

    def _calculate_fontsize(self, fontpath, str, size):

        image = Image.new('1', self.epd.size, WHITE)
        draw = ImageDraw.Draw(image)
        difference = size

        while True:
            font = ImageFont.truetype(fontpath, size)
            w, h = draw.textsize(str, font=font)
            difference = int(difference / 2)
            miss = self.epd.size[0] - w - 5
            logging.debug("Width: %d, Miss: %d, Difference: %d" %
                          (w, miss, difference))
            if difference <= 0:
                if miss < 0:
                    difference = 1
                else:
                    break

            if (miss > 0) and (miss < 10):
                break

            if miss < 10:
                size -= difference
            else:
                size += difference

        return font

    def _update_timezone(self):
        self.timezone = timezone(self.timezones[self.timezone_index])

    def _update_fonts(self):
        self.settings['font_index'] = int(self.settings['font_index'] %
                                          len(self.possible_fonts))
        font = self.possible_fonts[self.settings['font_index']]['filename']
        print "Changing font to " + self.possible_fonts[
            self.settings['font_index']]['title']
        #        if self.settings['twentyfour']:
        #            string = "23:59"
        #        else:
        string = "12:22 PM"
        self.clock_font = self._calculate_fontsize(font, string, 100)
        string = "Wednesday September 29th"
        self.date_font = self._calculate_fontsize(font, string, 42)
        self.icon_font = ImageFont.truetype(
            '/usr/share/fonts/truetype/WeatherIcons/weathericons-regular-webfont.ttf',
            46)
        self.weather_font = ImageFont.truetype(font, 42)
        self.menu_font = ImageFont.truetype(font, 38)

    def _play_tone_once(self):
        try:
            self.mpg123.kill()
        except:
            pass
        finally:
            self.mpg123 = subprocess.Popen([
                'mpg123', '--quiet', '--mono',
                self.possible_tones[self.settings['tone_index']]['filename']
            ],
                                           close_fds=True)

    def change_font(self, count):
        font_index = self.settings['font_index']
        font_index += count
        font_index = font_index % len(self.possible_fonts)
        self._set_setting('font_index', font_index)
        self._update_fonts()
        return True

    def change_tone(self, count):
        self._set_setting('tone_index', self.settings['tone_index'] + count)
        self._play_tone_once()
        return True

    def snooze_button(self, button):
        self.stop_alarming()

        if self.snooze_value == 0:
            self.snooze = 0
            self.mode = 'weather'
            print "Alarm stopped until tomorrow..."
            return True

        now = self.utc.localize(datetime.today())
        now = now.astimezone(self.timezone)
        # Set snooze to snooze_value from now
        self.snooze = (
            (now.hour * 60) +
            now.minute) - self.settings['alarm_time'] + self.snooze_value
        print 'Snoozing!'
        self.mode = 'weather'
        return True

    def start_alarming(self):
        self.alarming = True
        GPIO.output("CSID1", GPIO.HIGH)
        try:
            self.mpg123.kill()
        except:
            pass
        finally:
            self.mpg123 = subprocess.Popen([
                'mpg123', '--quiet', '--mono', '--loop', '-1',
                self.possible_tones[self.settings['tone_index']]['filename']
            ],
                                           close_fds=True)

    def stop_alarming(self):
        self.alarming = False
        GPIO.output("CSID1", GPIO.LOW)
        try:
            self.mpg123.kill()
        except:
            pass

    def snooze_turn(self, count):
        self.snooze_value += count
        if self.snooze_value < 0:
            self.snooze_value = 0
        return True

    def main_button(self, button):
        if self.alarming:
            now = self.utc.localize(datetime.today())
            now = now.astimezone(self.timezone)
            # Set snooze to 5 minutes from now
            self.snooze = (
                (now.hour * 60) + now.minute) - self.settings['alarm_time'] + 7
            print 'Snoozing!'
            self.stop_alarming()
        else:
            self.mode = 'menu'
            self.menuItem = 'alarm'
        return False

    def main_knob(self, count):
        if self.alarming or self.snooze > 0:
            self.stop_alarming()
            self.mode = 'snooze'
            self.snooze_value = 5
            return self.snooze_turn(count)
        else:
            self.mode = 'menu'
            self.menuItem = 'alarmOnly'
            return False

    menu = {
        # Main menu
        'settings': {
            'text': "Settings",
            'subArrow': True,
            'buttonAction': 'goto',
            'buttonParam': 'setting-24h',
            'next': 'alarm',
            'prev': 'exit'
        },
        'alarm': {
            'text': "Alarm",
            'subArrow': False,
            'buttonAction': 'toggle',
            'buttonParam': 'alarm',
            'next': 'set-alarm',
            'prev': 'settings'
        },
        'set-alarm': {
            'text': "Set Alarm",
            'subArrow': True,
            'buttonAction': 'mode',
            'buttonParam': 'set',
            'next': 'exit',
            'prev': 'alarm'
        },
        'exit': {
            'text': "Exit",
            'subArrow': False,
            'buttonAction': 'mode',
            'buttonParam': 'weather',
            'next': 'settings',
            'prev': 'set-alarm'
        },

        # Settings menu
        'setting-24h': {
            'text': "24 Hour",
            'subArrow': False,
            'buttonAction': 'toggle',
            'buttonParam': 'twentyfour',
            'next': 'setting-alarm-tone',
            'prev': 'setting-exit'
        },
        'setting-alarm-tone': {
            'text': "Ringtone",
            'subArrow': True,
            'buttonAction': 'mode',
            'buttonParam': 'tone',
            'next': 'setting-font',
            'prev': 'setting-24h'
        },
        'setting-font': {
            'text': "Font",
            'subArrow': True,
            'buttonAction': 'mode',
            'buttonParam': 'font',
            'next': 'setting-exit',
            'prev': 'setting-alarm-tone'
        },
        'setting-exit': {
            'text': "Exit",
            'subArrow': False,
            'buttonAction': 'mode',
            'buttonParam': 'weather',
            'next': 'setting-24h',
            'prev': 'setting-font'
        },

        # Alarm-only (Knob turned, not in menu)
        'alarmOnly': {
            'text': "Alarm",
            'subArrow': False,
            'buttonAction': 'toggle',
            'buttonParam': 'alarm',
            'next': 'alarmOnly-return',
            'prev': 'alarmOnly-return'
        },
        'alarmOnly-return': {
            'text': "Exit",
            'subArrow': False,
            'buttonAction': 'mode',
            'buttonParam': 'weather',
            'next': 'alarmOnly',
            'prev': 'alarmOnly',
        },
    }

    def draw_tone(self, draw, width, height, time_date_bottom):

        avail = height - time_date_bottom - 2

        tonestr = "> " + self.possible_tones[
            self.settings['tone_index']]['title']
        w, h = draw.textsize(tonestr, font=self.date_font)

        vspace = (avail - h) / 2
        y = time_date_bottom + vspace
        x = 8

        draw.text((x, y), tonestr, fill=BLACK, font=self.date_font)

        x += w

        try:
            now = time.time()
            if now - self.lastAction > 15:
                self.mode = 'weather'
        except:
            self.lastAction = time.time()

        return False

    def draw_font(self, draw, width, height, time_date_bottom):

        avail = height - time_date_bottom - 2

        fontstr = "> " + self.possible_fonts[
            self.settings['font_index']]['title']
        w, h = draw.textsize(fontstr, font=self.date_font)

        vspace = (avail - h) / 2
        y = time_date_bottom + vspace
        x = 8

        draw.text((x, y), fontstr, fill=BLACK, font=self.date_font)

        x += w

        try:
            now = time.time()
            if now - self.lastAction > 15:
                self.mode = 'weather'
        except:
            self.lastAction = time.time()

        return False

    def draw_menu(self, draw, width, height, time_date_bottom):

        avail = height - time_date_bottom - 2

        menustr = "> " + AlarmClock.menu[self.menuItem]['text']
        w, h = draw.textsize(menustr, font=self.menu_font)

        vspace = (avail - h) / 2
        y = time_date_bottom + vspace
        x = 8

        draw.text((x, y), menustr, fill=BLACK, font=self.menu_font)

        x += w

        if AlarmClock.menu[self.menuItem]['subArrow']:
            arrowstr = unichr(0xf04d)
            draw.text((x, y - 8), arrowstr, fill=BLACK, font=self.icon_font)
        elif AlarmClock.menu[self.menuItem]['buttonAction'] == 'toggle':
            param = AlarmClock.menu[self.menuItem]['buttonParam']
            checktxt = " - " + ("ON" if self.settings[param] else "OFF")
            draw.text((x, y), checktxt, fill=BLACK, font=self.menu_font)

        try:
            now = time.time()
            if now - self.lastAction > 15:
                self.mode = 'weather'
        except:
            self.lastAction = time.time()

        return False

    def menu_turn(self, count):
        while (count > 0):
            self.menuItem = AlarmClock.menu[self.menuItem]['next']
            count -= 1

        while (count < 0):
            self.menuItem = AlarmClock.menu[self.menuItem]['prev']
            count += 1

        return False

    def menu_button(self, button):
        entry = AlarmClock.menu[self.menuItem]
        action = entry['buttonAction']
        param = entry['buttonParam']

        if action == 'toggle':
            self._set_setting(param, not self.settings[param])
        elif action == 'mode':
            self.mode = param
            return True
        elif action == 'goto':
            self.menuItem = param

        return False

    def draw_snooze(self, draw, width, height, time_date_bottom):
        avail = height - time_date_bottom - 2

        if self.snooze_value == 0:
            snooze_str = "Stop Alarm"
        else:
            snooze_str = "Snooze: "
            snooze_str += "%d Min" % self.snooze_value

        w, h = draw.textsize(snooze_str, font=self.menu_font)

        vspace = (avail - h) / 2
        y = time_date_bottom + vspace
        x = 8

        draw.text((x, y), snooze_str, fill=BLACK, font=self.menu_font)

        x += w

        try:
            now = time.time()
            if now - self.lastAction > 15:
                self.snooze_button(None)
        except:
            self.lastAction = time.time()

        return False

    def draw_set(self, draw, width, height, time_date_bottom):
        avail = height - time_date_bottom - 2

        try:
            alarm_time = self.settings['alarm_time']
        except:
            alarm_time = 360

        alarm_hour = alarm_time / 60
        alarm_minute = alarm_time % 60

        alarmstr = "Alarm: "

        if self.settings['twentyfour']:
            alarmstr += "%d:%02d" % (alarm_hour, alarm_minute)
        else:
            alarmstr += "%d:%02d" % (
                (lambda x: 12 if x == 0 else x)(alarm_hour % 12), alarm_minute)
            if alarm_hour >= 12:
                alarmstr += " PM"
            else:
                alarmstr += " AM"

        w, h = draw.textsize(alarmstr, font=self.menu_font)

        vspace = (avail - h) / 2
        y = time_date_bottom + vspace
        x = 8

        draw.text((x, y), alarmstr, fill=BLACK, font=self.menu_font)

        x += w

        try:
            now = time.time()
            if now - self.lastAction > 15:
                self.mode = 'weather'
        except:
            self.lastAction = time.time()

        return False

    def set_turn(self, count):
        if count > 10:
            count *= 10
        elif count > 5:
            count *= 5

        try:
            alarm_time = self.settings['alarm_time']
        except:
            alarm_time = 360
        self._set_setting('alarm_time', (alarm_time + count) % 1440)
        return False

    def set_button(self, button):
        self.mode = 'weather'
        logging.debug("Set alarm value to %d" % self.settings['alarm_time'])
        return True

    def draw_weather(self, draw, width, height, time_date_bottom):

        # The moon phase font represents the phase as a unicode glyph between f0d0 and f0eb
        # with the new moon at the end
        #
        # The DarkSky API represents the new moon as 0, the full moon as 0.5, and the rest
        # as a decimal fraction.
        #
        # To render the correct glpyh, first convert the fraction into an index between 0-28,
        # Subtract 1, and treat negative as a new moon...
        moonPhase = self.forecast.daily().data[0].moonPhase
        moonPhase *= 28.0
        moonPhase = int(round(moonPhase)) - 1
        if moonPhase < 0:
            moonPhase = 27
        moonstr = unichr(moonPhase + 0xf0d0)

        # weather
        avail = height - 2 - time_date_bottom

        iconstr = AlarmClock.weather_icons[self.forecast.hourly().icon]
        iw, ih = draw.textsize(iconstr, font=self.icon_font)

        tempstr = u"%d\u00B0" % (
            self.forecast.currently().apparentTemperature
        )  #, self.forecast.daily().data[0].apparentTemperatureMin, self.forecast.daily().data[0].apparentTemperatureMax)
        tw, th = draw.textsize(tempstr, font=self.weather_font)

        lowstr = u"Low:%d\u00B0" % self.forecast.daily(
        ).data[0].apparentTemperatureMin
        lw, lh = draw.textsize(lowstr, font=self.date_font)
        highstr = u"High:%d\u00B0" % self.forecast.daily(
        ).data[0].apparentTemperatureMax
        hw, hh = draw.textsize(highstr, font=self.date_font)
        mw, mh = draw.textsize(moonstr, font=self.icon_font)

        hlw = max(hw, lw)
        line_width = iw + tw + mw + max(hw, lw)
        horiz_space = (width - 4 - line_width) / 5

        x = horiz_space + 2
        y = time_date_bottom + ((avail - ih) / 2)
        draw.text((x, y), iconstr, fill=BLACK, font=self.icon_font)
        x += iw + horiz_space
        y = time_date_bottom + ((avail - th) / 2)
        draw.text((x, y), tempstr, fill=BLACK, font=self.weather_font)
        x += tw + horiz_space
        hlvspace = (avail - hh - lh) / 2
        y = time_date_bottom + hlvspace
        draw.text((x, y), highstr, fill=BLACK, font=self.date_font)
        y += hh
        draw.text((x, y), lowstr, fill=BLACK, font=self.date_font)
        x += hlw + horiz_space
        y = time_date_bottom + ((avail - mh) / 2)
        draw.text((x, y), moonstr, fill=BLACK, font=self.icon_font)

    def turn_cb(self, address, pips):
        self.lock.acquire()
        self.pips += pips
        self.lock.release()

    def press_cb(self, address, presses):
        self.lock.acquire()
        self.presses += presses
        self.lock.release()

    def run(self):
        self.utc = timezone('UTC')

        blinked = False

        # initially set all white background
        image = Image.new('1', self.epd.size, WHITE)

        # prepare for drawing
        draw = ImageDraw.Draw(image)
        width, height = image.size

        # initial time
        self._update_timezone()
        now = self.utc.localize(datetime.today())
        now = now.astimezone(self.timezone)
        full_update = True

        self._update_fonts()

        while True:
            prev = now

            start = time.time()
            # clear the display buffer
            draw.rectangle((0, 0, width, height), fill=WHITE, outline=WHITE)

            t_rect = time.time()

            # border
            draw.rectangle((1, 1, width - 1, height - 1),
                           fill=WHITE,
                           outline=BLACK)
            draw.rectangle((2, 2, width - 2, height - 2),
                           fill=WHITE,
                           outline=BLACK)

            t_border = time.time()

            if self.settings['twentyfour']:
                timefmt = "%-H:%M"
            else:
                timefmt = "%-I:%M %p"

            # Time & Date get 2/3rds of the display...
            daystr = '{dt:%A}, {dt:%B} {dt.day}'.format(dt=now)
            dw, dh = draw.textsize(daystr, font=self.date_font)
            timestr = now.strftime(timefmt)
            tw, th = draw.textsize(timestr, font=self.clock_font)
            avail = int((height - 4) * 0.60)
            space = (avail - dh - th) / 2
            time_date_bottom = avail + 2

            y = space + 2
            draw.text(((width - tw) / 2, y),
                      timestr,
                      fill=BLACK,
                      font=self.clock_font)

            y += th
            y += space
            draw.text(((width - dw) / 2, y),
                      daystr,
                      fill=BLACK,
                      font=self.date_font)

            t_clock = time.time()

            try:
                logging.debug(
                    "Calling " +
                    str(AlarmClock.modes[self.mode]['render_bottom']) +
                    " Mode: %s" % self.mode)
                AlarmClock.modes[self.mode]['render_bottom'](self, draw, width,
                                                             height,
                                                             time_date_bottom)
            except Exception as e:
                print e
                pass

            t_bottom = time.time()

            # display image on the panel
            self.epd.display(image)
            if not full_update:
                self.epd.partial_update()
            else:
                full_update = False
                self.epd.update()

            t_update = time.time()

            if (now.minute % 5) == 0:
                if blinked == False:
                    blinked = True
                    self.epd.blink()
                    self._update_weather()
            else:
                blinked = False

            logging.debug("Clear: %s, Draw Border: %s, Draw Clock: %s, Draw Bottom: %s, Update: %s" % \
                          (str(t_rect - start), str(t_border - t_rect), str(t_clock - t_border), str(t_bottom - t_clock), str(t_update - t_bottom)))

            # wait for next minute
            i = 0
            while True:
                if (i % 100) == 0:  # Only call today() once every 100 loops
                    now = self.utc.localize(datetime.today())
                    now = now.astimezone(self.timezone)
                    if now.hour != prev.hour or now.minute != prev.minute:
                        alarm_time = (self.settings['alarm_time'] +
                                      self.snooze) % 1440
                        if self.settings['alarm'] and (
                            (now.hour * 60) + now.minute) == alarm_time:
                            self.start_alarming()
                            print "Alarming!"
                        elif self.settings['alarm']:
                            value = ((now.hour * 60) + now.minute)
                            logging.debug(
                                "Waiting until %d to alarm (Currently: %d)" %
                                (alarm_time, value))

                        break
                i += 1

                self.lock.acquire()
                input = self.pips
                button = self.presses
                self.pips = 0
                self.presses = 0
                self.lock.release()
                if input or button:
                    print(input, button)
                if input != 0:
                    self.lastAction = time.time()
                    logging.debug("Calling " +
                                  str(AlarmClock.modes[self.mode]['knob']) +
                                  " Mode: %s, Val: %d" % (self.mode, input))
                    full_update = AlarmClock.modes[self.mode]['knob'](self,
                                                                      input)
                    break
                if button != 0:
                    full_update = AlarmClock.modes[self.mode]['button'](self,
                                                                        button)
                    break
                time.sleep(0.01)

    modes = {
        'weather': {
            'render_bottom': draw_weather,
            'knob': main_knob,
            'button': main_button
        },
        'menu': {
            'render_bottom': draw_menu,
            'knob': menu_turn,
            'button': menu_button
        },
        'set': {
            'render_bottom': draw_set,
            'knob': set_turn,
            'button': set_button
        },
        'font': {
            'render_bottom': draw_font,
            'knob': change_font,
            'button': main_button
        },
        'tone': {
            'render_bottom': draw_tone,
            'knob': change_tone,
            'button': main_button
        },
        'snooze': {
            'render_bottom': draw_snooze,
            'knob': snooze_turn,
            'button': snooze_button
        },
    }