Initial public release of PieTime!

\o/
This commit is contained in:
2016-02-07 15:41:31 +01:00
commit 0912fd15e8
40 changed files with 4838 additions and 0 deletions

12
pie_time/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
__title__ = 'pie_time'
__version__ = '1.0'
__author__ = u'Tomek Wójcik'
__license__ = 'MIT'
__copyright__ = (
u'Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>'
)
from .application import PieTime
from .card import AbstractCard

511
pie_time/application.py Normal file
View File

@@ -0,0 +1,511 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
"""
pie_time.application
====================
This module implements the PieTime application.
"""
import datetime
import argparse
import imp
import logging
import os
import sys
import pygame
from pie_time import __copyright__ as copyright, __version__ as version
RET_OK = 0
RET_ERROR = 99
MOTD_PICLOCK_BANNER = u"PieTime v%s by Tomek Wójcik" % (
version
)
MOTD_LICENSE_BANNER = u"Released under the MIT license"
EVENT_QUIT = 0
EVENT_CLICK_TO_UNBLANK = 1
EVENT_CLICK_TO_PREV_CARD = 2
EVENT_CLICK_TO_NEXT_CARD = 3
class Quit(Exception):
pass
class PieTimeEvent(object):
def __init__(self, app, event):
self.event = event
self.app = app
def is_quit(self):
return (self.event.type == pygame.QUIT)
def is_key_quit(self):
return (
self.event.type == pygame.KEYDOWN
and self.event.key == self.app.KEY_QUIT
)
def is_click_to_unblank(self):
return (
self.event.type == pygame.MOUSEBUTTONDOWN
and self.app._click_to_unblank_interval is not None
and self.app._is_blanked is True
)
def is_click_to_prev_card(self):
return (
self.event.type == pygame.MOUSEBUTTONDOWN
and self.app._click_to_transition is True
and self.app._is_blanked is False
and self.app._ctt_region_prev.collidepoint(self.event.pos) == 1
)
def is_click_to_next_card(self):
return (
self.event.type == pygame.MOUSEBUTTONDOWN
and self.app._click_to_transition is True
and self.app._is_blanked is False
and self.app._ctt_region_next.collidepoint(self.event.pos) == 1
)
class PieTime(object):
"""
The PieTime application.
:param deck: the deck
:param screen_size: tuple of (width, height) to use as the screen size
:param fps: number of frames per second to limit rendering to
:param blanker_schedule: blanker schedule
:param click_to_unblank_interval: time interval for click to unblank
:param click_to_transition: boolean defining if click to transition is
enabled
:param verbose: boolean defining if verbose logging should be on
:param log_path: path to log file (if omitted, *stdout* will be used)
"""
#: Default background color
BACKGROUND_COLOR = (0, 0, 0)
#: Blanked screen color
BLANK_COLOR = (0, 0, 0)
#: Default card display duration interval
CARD_INTERVAL = 60
#: Defines key which quits the application
KEY_QUIT = pygame.K_ESCAPE
#: Defines size of click to transition region square
CLICK_TO_TRANSITION_REGION_SIZE = 30
_DEFAULT_OUTPUT_STREAM = sys.stdout
_STREAM_FACTORY = file
def __init__(self, deck, screen_size=(320, 240), fps=20,
blanker_schedule=None, click_to_unblank_interval=None,
click_to_transition=True, verbose=False, log_path=None):
self._deck = deck
#: The screen surface
self.screen = None
#: The screen size tuple
self.screen_size = screen_size
#: List of events captured in this frame
self.events = []
#: Path to log file. If `None`, *stdout* will be used for logging.
self.log_path = log_path
self._fps = fps
self._verbose = verbose
self._blanker_schedule = blanker_schedule
self._click_to_unblank_interval = click_to_unblank_interval
self._click_to_transition = click_to_transition
self._clock = None
self._cards = []
self._is_blanked = False
self._current_card_idx = None
self._current_card_time = None
self._should_quit = False
self._internal_events = set()
self._ctu_timer = None
self._output_stream = None
self._ctt_region_prev = pygame.Rect(
0,
self.screen_size[1] - self.CLICK_TO_TRANSITION_REGION_SIZE,
self.CLICK_TO_TRANSITION_REGION_SIZE,
self.CLICK_TO_TRANSITION_REGION_SIZE
)
self._ctt_region_next = pygame.Rect(
self.screen_size[0] - self.CLICK_TO_TRANSITION_REGION_SIZE,
self.screen_size[1] - self.CLICK_TO_TRANSITION_REGION_SIZE,
self.CLICK_TO_TRANSITION_REGION_SIZE,
self.CLICK_TO_TRANSITION_REGION_SIZE
)
def _should_blank(self, now=None):
if self._has_click_to_unblank_event() or self._ctu_timer is not None:
if self._is_blanked is False and self._ctu_timer is None:
self._ctu_timer = None
return False
if self._click_to_unblank_interval is not None:
if self._ctu_timer is None:
self._ctu_timer = self._click_to_unblank_interval
return False
self._ctu_timer -= self._clock.get_time() / 1000.0
if self._ctu_timer <= 0:
self._ctu_timer = None
return True
else:
return False
if self._blanker_schedule:
delta_blanker_start, delta_blanker_end = self._blanker_schedule
if now is None:
now = datetime.datetime.now()
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
blanker_start = midnight + delta_blanker_start
blanker_end = midnight + delta_blanker_end
if blanker_start > blanker_end:
if now.hour < 12:
blanker_start -= datetime.timedelta(days=1)
else:
blanker_end += datetime.timedelta(days=1)
if now >= blanker_start and now < blanker_end:
return True
return False
def _blank(self):
if self._is_blanked is False:
self.logger.debug('Blanking the screen!')
self.will_blank()
self._is_blanked = True
self.screen.fill(self.BLANK_COLOR)
def _unblank(self):
if self._is_blanked:
self.logger.debug('Unblanking the screen!')
self.will_unblank()
self._is_blanked = False
self._current_card_idx = 0
self._current_card_time = 0
self._cards[self._current_card_idx][0].show()
def _transition_cards(self, direction=1, force=False):
if self._current_card_idx is None and force is False:
self._current_card_idx = 0
self._current_card_time = 0
self._cards[self._current_card_idx][0].show()
elif len(self._cards) > 1:
self._current_card_time += self._clock.get_time() / 1000.0
card_interval = self._cards[self._current_card_idx][1]
if self._current_card_time >= card_interval or force is True:
new_card_idx = self._current_card_idx + direction
if new_card_idx >= len(self._cards):
new_card_idx = 0
elif new_card_idx < 0:
new_card_idx = len(self._cards) - 1
self.logger.debug('Card transition: %d -> %d' % (
self._current_card_idx, new_card_idx
))
self._cards[self._current_card_idx][0].hide()
self._current_card_idx = new_card_idx
self._current_card_time = 0
self._cards[self._current_card_idx][0].show()
def _get_events(self):
self._internal_events = set()
self.events = []
for event in pygame.event.get():
pie_time_event = PieTimeEvent(self, event)
if pie_time_event.is_quit():
self.logger.debug('_get_events: QUIT')
self._internal_events.add(EVENT_QUIT)
elif pie_time_event.is_key_quit():
self.logger.debug('_get_events: KEY_QUIT')
self._internal_events.add(EVENT_QUIT)
elif pie_time_event.is_click_to_unblank():
self.logger.debug('_get_events: CLICK_TO_UNBLANK')
self._internal_events.add(EVENT_CLICK_TO_UNBLANK)
elif pie_time_event.is_click_to_prev_card():
self.logger.debug('_get_events: CLICK_TO_PREV_CARD')
self._internal_events.add(EVENT_CLICK_TO_PREV_CARD)
elif pie_time_event.is_click_to_next_card():
self.logger.debug('_get_events: CLICK_TO_NEXT_CARD')
self._internal_events.add(EVENT_CLICK_TO_NEXT_CARD)
else:
self.events.append(event)
def _has_quit_event(self):
return (EVENT_QUIT in self._internal_events)
def _has_click_to_unblank_event(self):
return (EVENT_CLICK_TO_UNBLANK in self._internal_events)
def _start_clock(self):
self._clock = pygame.time.Clock()
def _setup_output_stream(self):
if self.log_path is None:
self._output_stream = self._DEFAULT_OUTPUT_STREAM
else:
self._output_stream = self._STREAM_FACTORY(self.log_path, 'a')
def _setup_logging(self):
logger = logging.getLogger('PieTime')
requests_logger = logging.getLogger('requests')
if self._verbose:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
requests_logger.setLevel(logging.WARNING)
handler = logging.StreamHandler(self._output_stream)
formatter = logging.Formatter(
'%(asctime)s PieTime: %(levelname)s: %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
for requests_handler in requests_logger.handlers:
requests_logger.removeHandler(requests_handler)
requests_logger.addHandler(handler)
@property
def logger(self):
"""The application-wide :py:class:`logging.Logger` object."""
if not hasattr(self, '_logger'):
self._logger = logging.getLogger('PieTime')
return self._logger
def init_pygame(self):
"""Initializes PyGame and the internal clock."""
self.logger.debug('Initializing PyGame.')
pygame.init()
pygame.mouse.set_visible(False)
def quit_pygame(self):
"""Quits PyGame."""
self.logger.debug('Quitting PyGame.')
pygame.quit()
self._clock = None
def init_cards(self):
"""
Initializes the cards.
Initialization of a card consits of the following steps:
* Creating an instance of the card class,
* Binding the card with the application
(:py:meth:`pie_time.AbstractCard.set_app`),
* Setting the card's settings
(:py:meth:`pie_time.AbstractCard.set_settings`),
* Initializing the card (:py:meth:`pie_time.AbstractCard.initialize`).
"""
self.logger.debug('Initializing cards.')
for i in xrange(0, len(self._deck)):
card_def = self._deck[i]
klass = None
interval = self.CARD_INTERVAL
settings = {}
if not isinstance(card_def, tuple):
klass = card_def
elif len(card_def) == 2:
klass, interval = card_def
elif len(card_def) == 3:
klass, interval, settings = card_def
if klass is not None:
card = klass()
card.set_app(self)
card.set_settings(settings)
card.initialize()
self._cards.append((card, interval))
else:
self.logger.warning('Invalid deck entry at index: %d' % i)
def destroy_cards(self):
"""
Destroys the cards.
Calls the :py:meth:`pie_time.AbstractCard.quit` of each card.
"""
self.logger.debug('Destroying cards.')
while len(self._cards) > 0:
card, _ = self._cards.pop()
try:
card.quit()
except:
self.logger.error('ERROR!', exc_info=True)
def get_screen(self):
"""Creates and returns the screen screen surface."""
self.logger.debug('Creating screen.')
return pygame.display.set_mode(self.screen_size)
def fill_screen(self):
"""
Fills the screen surface with color defined in
:py:attr:`pie_time.PieTime.BACKGROUND_COLOR`.
"""
self.screen.fill(self.BACKGROUND_COLOR)
def run(self, standalone=True):
"""
Runs the application.
This method contains the app's main loop and it never returns. Upon
quitting, this method will call the :py:func:`sys.exit` function with
the status code (``99`` if an unhandled exception occurred, ``0``
otherwise).
The application will quit under one of the following conditions:
* An unhandled exception reached this method,
* PyGame requested to quit (e.g. due to closing the window),
* Some other code called the :py:meth:`pie_time.PieTime.quit` method on
the application.
Before quitting the :py:meth:`pie_time.PieTime.destroy_cards` and
:py:meth:`pie_time.PieTime.quit_pygame` methods will be called to clean
up.
"""
result = RET_OK
self._setup_output_stream()
self._setup_logging()
try:
self.logger.info(MOTD_PICLOCK_BANNER)
self.logger.info(copyright)
self.logger.info(MOTD_LICENSE_BANNER)
self.logger.debug('My PID = %d' % os.getpid())
self.init_pygame()
self.screen = self.get_screen()
self.init_cards()
self._start_clock()
while True:
self._get_events()
if self._should_quit or self._has_quit_event():
raise Quit()
if not self._should_blank():
self._unblank()
if EVENT_CLICK_TO_PREV_CARD in self._internal_events:
self._transition_cards(direction=-1, force=True)
elif EVENT_CLICK_TO_NEXT_CARD in self._internal_events:
self._transition_cards(direction=1, force=True)
else:
self._transition_cards()
card = self._cards[self._current_card_idx][0]
card.tick()
self.fill_screen()
self.screen.blit(
card.surface, (0, 0, card.width, card.height)
)
else:
self._blank()
pygame.display.flip()
self._clock.tick(self._fps)
except Exception as exc:
if not isinstance(exc, Quit):
self.logger.error('ERROR!', exc_info=True)
result = RET_ERROR
finally:
self.destroy_cards()
self.quit_pygame()
if standalone:
sys.exit(result)
else:
return result
def quit(self):
"""Tells the application to quit."""
self._should_quit = True
def will_blank(self):
"""
Called before blanking the screen.
This method can be used to perform additional operations before
blanking the screen.
The default implementation does nothing.
"""
pass
def will_unblank(self):
"""
Called before unblanking the screen.
This method can be used to perform additional operations before
unblanking the screen.
The default implementation does nothing.
"""
pass

195
pie_time/card.py Normal file
View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
"""
pie_time.card
=============
This module contains the AbstractCard class.
"""
import os
import sys
import pygame
class AbstractCard(object):
"""
The abstract card class.
All the custom cards **must** inherit from this class.
**Application binding and settings.**
The application calls the card's :py:meth:`pie_time.AbstractCard.set_app`
and :py:meth:`pie_time.AbstractCard.set_settings` methods during
initialization (before calling the
:py:meth:`pie_time.AbstractCard.initialize` method).
The application reference is stored in ``_app`` attribute.
The settings dictionary is stored in ``_settings`` attribute and defaults
to an empty dictionary.
**Drawing**
All the drawing on the card's surface should be done in the
:py:meth:`pie_time.AbstractCard.tick` method. The method's implementation
should be as fast as possible to avoid throttling the FPS down.
**Resources**
The :py:meth:`pie_time.AbstractCard.path_for_resource` method can be used
to get an absolute path to a resource file. The card's resource folder
should be placed along with the module containing the card's class.
Name of the resource folder can be customized by overriding the
:py:attr:`pie_time.AbstractCard.RESOURCE_FOLDER` attribute.
"""
#: Name of the folder containing the resources
RESOURCE_FOLDER = 'resources'
def __init__(self):
self._app = None
self._settings = {}
self._surface = None
def set_app(self, app):
"""Binds the card with the *app*."""
self._app = app
def set_settings(self, settings):
"""Sets *settings* as the card's settings."""
self._settings = settings
@property
def width(self):
"""The card's surface width. Defaults to the app screen's width."""
return self._app.screen_size[0]
@property
def height(self):
"""The card's surface height. Defaults to the app screen's height."""
return self._app.screen_size[1]
@property
def surface(self):
"""
The cards surface. The surface width and height are defined by the
respective properties of the class.
"""
if self._surface is None:
self._surface = pygame.surface.Surface((self.width, self.height))
return self._surface
@property
def background_color(self):
"""
The background color. Defaults to
:py:attr:`pie_time.PieTime.BACKGROUND_COLOR`.
"""
return self._settings.get(
'background_color', self._app.BACKGROUND_COLOR
)
def path_for_resource(self, resource, folder=None):
"""
Returns an absolute path for *resource*. The optional *folder*
keyword argument allows specifying a subpath.
"""
_subpath = ''
if folder:
_subpath = folder
module_path = sys.modules[self.__module__].__file__
return os.path.join(
os.path.abspath(os.path.dirname(module_path)),
self.RESOURCE_FOLDER, _subpath, resource
)
def initialize(self):
"""
Initializes the card.
The application calls this method right after creating an instance of
the class.
This method can be used to perform additional initialization on the
card, e.g. loading resources, setting the initial state etc.
The default implementation does nothing.
"""
pass
def quit(self):
"""
Initializes the card.
This method can be used to perform additional cleanup on the
card, e.g. stop threads, free resources etc.
The default implementation does nothing.
"""
def show(self):
"""
Shows the card.
The application calls this method each time the card becomes the
current card.
This method can be used to reset initial state, e.g. sprite positions.
The default implementation does nothing.
"""
pass
def hide(self):
"""
Hides the card.
The application calls this method each time the card resignes the
current card.
This method can be used to e.g. stop threads which aren't supposed to
be running when the card isn't being displayed.
The default implementation does nothing.
"""
pass
def tick(self):
"""
Ticks the card.
The application calls this method on the current card in every main
loop iteration.
This method should be used to perform drawing and other operations
needed to properly display the card on screen.
Subclasses **must** override this method.
"""
raise NotImplementedError('TODO')

View File

@@ -0,0 +1,3 @@
from .clock import ClockCard
from .picture import PictureCard
from .weather import WeatherCard

154
pie_time/cards/clock.py Normal file
View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
"""
pie_time.cards.clock
====================
This module containse the ClockCard class.
"""
import datetime
import pygame
from pie_time.card import AbstractCard
class ClockCard(AbstractCard):
"""
The clock card.
This card displays a digital clock and date.
**Settings dictionary keys**:
* **time_format** (*string*) - time format string (*strftime()*
compatible). Defaults to :py:attr:`pie_time.cards.ClockCard.TIME_FORMAT`
* **time_blink** (*boolean*) - if set to ``True`` the semicolons will
blink. Defaults to ``True``.
* **time_color** (*tuple*) - time text color. Defaults to
:py:attr:`pie_time.cards.ClockCard.GREEN`
* **date_format** (*string*) - date format string (*strftime()*
compatible). Defaults to :py:attr:`pie_time.cards.ClockCard.DATE_FORMAT`
* **date_color** (*tuple*) - date text color. Defaults to
:py:attr:`pie_time.cards.ClockCard.GREEN`
"""
#: Green color for text
GREEN = (96, 253, 108)
#: Default time format
TIME_FORMAT = '%I:%M %p'
#: Default date format
DATE_FORMAT = '%a, %d %b %Y'
def initialize(self):
self._time_font = pygame.font.Font(
self.path_for_resource('PTM55FT.ttf'), 63
)
self._date_font = pygame.font.Font(
self.path_for_resource('opensans-light.ttf'), 36
)
self._now = None
self._current_interval = 0
def _render_time(self, now):
time_format = self._settings.get('time_format', self.TIME_FORMAT)
if self._settings.get('time_blink', True) and now.second % 2 == 1:
time_format = time_format.replace(':', ' ')
current_time = now.strftime(time_format)
text = self._time_font.render(
current_time, True, self._settings.get('time_color', self.GREEN)
)
return text
def _render_date(self, now):
date_format = self._settings.get('date_format', self.DATE_FORMAT)
current_date = now.strftime(date_format)
text = self._date_font.render(
current_date, True, self._settings.get('date_color', self.GREEN)
)
return text
def _update_now(self):
if self._now is None:
self._now = datetime.datetime.now()
self._current_interval = 0
return True
else:
self._current_interval += self._app._clock.get_time()
if self._current_interval >= 1000:
self._now = datetime.datetime.now()
self._current_interval = self._current_interval - 1000
return True
return False
def show(self):
self._now = None
def tick(self):
now_updated = self._update_now()
if now_updated:
time_text = self._render_time(self._now)
date_text = self._render_date(self._now)
time_text_size = time_text.get_size()
date_text_size = date_text.get_size()
time_text_origin_y = (
(self.height - time_text_size[1] - date_text_size[1]) / 2.0
)
time_text_rect = (
(self.width - time_text_size[0]) / 2.0,
time_text_origin_y,
time_text_size[0],
time_text_size[1]
)
date_text_rect = (
(self.width - date_text_size[0]) / 2.0,
time_text_origin_y + time_text_size[1],
date_text_size[0],
date_text_size[1]
)
self.surface.fill(self.background_color)
self.surface.blit(time_text, time_text_rect)
self.surface.blit(date_text, date_text_rect)

140
pie_time/cards/picture.py Normal file
View File

@@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
"""
pie_time.cards.picture
======================
This module containse the PictureCard class.
"""
import cStringIO
import os
import urlparse
import pygame
import requests
from pie_time.card import AbstractCard
class PictureCard(AbstractCard):
"""
The picture card.
This cards displays a picture from list of pre-defined pictures. If more
than one picture is defined, it's changed each time the card transitions
to current card.
**Settings dictionary keys**:
* **urls** (*list*) - **required** list of picture URLs. Currently, only
``file://``, ``http://`` and ``https://`` URL schemes are supported.
"""
def initialize(self):
self._pictures = []
self._current_picture_idx = None
self._should_redraw = True
for url in self._settings['urls']:
self._pictures.append(self._load_picture(url))
def _load_picture(self, url):
self._app.logger.debug(
'PictureCard: Attempting to load picture: %s' % url
)
parsed_url = urlparse.urlparse(url)
surface = None
try:
format = None
if parsed_url.scheme == 'file':
surface = pygame.image.load(parsed_url.path)
_, ext = os.path.splitext(parsed_url.path)
format = ext.lower()
elif parsed_url.scheme.startswith('http'):
rsp = requests.get(url)
assert rsp.status_code == 200
format = rsp.headers['Content-Type'].replace('image/', '')
surface = pygame.image.load(
cStringIO.StringIO(rsp.content), 'picture.%s' % format
)
if surface and format:
if format.lower().endswith('png'):
surface = surface.convert_alpha(self._app.screen)
else:
surface = surface.convert(self._app.screen)
except Exception as exc:
self._app.logger.error(
'PictureCard: Could not load picture: %s' % url, exc_info=True
)
return surface
def show(self):
if len(self._pictures) == 0:
self._current_picture_idx = None
elif len(self._pictures) == 1:
self._current_picture_idx = 0
else:
if self._current_picture_idx is None:
self._current_picture_idx = 0
else:
new_picture_idx = self._current_picture_idx + 1
if new_picture_idx >= len(self._pictures):
new_picture_idx = 0
self._app.logger.debug(
'PictureCard: Picture transition %d -> %d' % (
self._current_picture_idx, new_picture_idx
)
)
self._current_picture_idx = new_picture_idx
self._should_redraw = True
def tick(self):
if self._should_redraw:
self.surface.fill(self.background_color)
if self._current_picture_idx is not None:
picture = self._pictures[self._current_picture_idx]
picture_size = picture.get_size()
picture_rect = picture.get_rect()
if picture_size != self._app.screen_size:
picture_rect = (
(self.width - picture_size[0]) / 2.0,
(self.height - picture_size[1]) / 2.0,
picture_size[0], picture_size[1]
)
self.surface.blit(picture, picture_rect)
self._should_redraw = False

Binary file not shown.

Binary file not shown.

Binary file not shown.

286
pie_time/cards/weather.py Normal file
View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
"""
pie_time.cards.weather
======================
This module containse the WeatherCard class.
"""
from threading import Timer
import pygame
import requests
from pie_time.card import AbstractCard
URL_TEMPLATE = (
'http://api.openweathermap.org/data/2.5/weather?q=%s&units=%s&APPID=%s'
)
class WeatherCard(AbstractCard):
"""
The weather card.
This cards displays the current weather for a selected city. The weather
information is obtained from OpenWeatherMap.
**Settings dictionary keys**:
* **api_key** (*string*) - **required** API key.
* **city** (*string*) - **required** name of the city.
* **units** (*string*) - units name (``metric`` or ``imperial``). Defaults
to :py:attr:`pie_time.cards.WeatherCard.UNITS`
* **refresh_interval** (*int*) - refresh interval in seconds. Defaults to
:py:attr:`pie_time.cards.WeatherCard.REFRESH_INTERVAL`
* **city_color** (*tuple*) - city text color. Defaults to
:py:attr:`pie_time.cards.WeatherCard.WHITE`
* **icon_color** (*tuple*) - icon text color. Defaults to
:py:attr:`pie_time.cards.WeatherCard.WHITE`
* **temperature_color** (*tuple*) - temperature text color. Defaults to
:py:attr:`pie_time.cards.WeatherCard.WHITE`
* **conditions_color** (*tuple*) - conditions text color. Defaults to
:py:attr:`pie_time.cards.WeatherCard.WHITE`
"""
#: Default units
UNITS = 'metric'
#: Default refresh interval
REFRESH_INTERVAL = 600
#: White color for text
WHITE = (255, 255, 255)
WEATHER_CODE_TO_ICON = {
'01d': u'',
'01n': u'',
'02d': u'',
'02n': u'',
'03d': u'',
'03n': u'',
'04d': u'',
'04n': u'',
'09d': u'',
'09n': u'',
'10d': u'',
'10n': u'',
'11d': u'',
'11n': u'',
'13d': u'',
'13n': u'',
'50d': u'',
'50n': u''
}
ICON_SPACING = 24
def initialize(self, refresh=True):
assert 'api_key' in self._settings,\
'Configuration error: missing API key'
assert 'city' in self._settings, 'Configuration error: missing city'
self._text_font = pygame.font.Font(
self.path_for_resource('opensans-light.ttf'), 30
)
self._temp_font = pygame.font.Font(
self.path_for_resource('opensans-light.ttf'), 72
)
self._icon_font = pygame.font.Font(
self.path_for_resource('linea-weather-10.ttf'), 128
)
self._timer = None
self._current_conditions = None
self._should_redraw = True
if refresh:
self._refresh_conditions()
def _refresh_conditions(self):
self._app.logger.debug('Refreshing conditions.')
self._timer = None
try:
rsp = requests.get(
URL_TEMPLATE % (
self._settings['city'],
self._settings.get('units', self.UNITS),
self._settings['api_key']
)
)
if rsp.status_code != 200:
self._app.logger.error(
'WeatherCard: Received HTTP %d' % rsp.status_code
)
else:
try:
payload = rsp.json()
self._current_conditions = {
'conditions': payload['weather'][0]['main'],
'icon': payload['weather'][0].get('icon', None),
'temperature': payload['main']['temp']
}
self._should_redraw = True
except:
self._app.logger.error(
'WeatherCard: ERROR!', exc_info=True
)
except:
self._app.logger.error('WeatherCard: ERROR!', exc_info=True)
self._timer = Timer(
self._settings.get('refresh_interval', self.REFRESH_INTERVAL),
self._refresh_conditions
)
self._timer.start()
def _render_city(self):
city_text = self._text_font.render(
self._settings['city'], True,
self._settings.get('city_color', self.WHITE)
)
return city_text
def _render_conditions(self):
conditions_text = self._text_font.render(
self._current_conditions['conditions'].capitalize(),
True, self._settings.get('conditions_color', self.WHITE)
)
return conditions_text
def _render_icon(self):
icon = self._current_conditions['icon']
weather_icon = None
if icon in self.WEATHER_CODE_TO_ICON:
weather_icon = self._icon_font.render(
self.WEATHER_CODE_TO_ICON[icon],
True, self._settings.get('icon_color', self.WHITE)
)
return weather_icon
def _render_temperature(self):
temp_text = self._temp_font.render(
u'%d°' % self._current_conditions['temperature'],
True,
self._settings.get('temperature_color', self.WHITE)
)
return temp_text
def quit(self):
if self._timer is not None:
self._timer.cancel()
def tick(self):
if self._should_redraw:
self.surface.fill(self.background_color)
city_text = self._render_city()
city_text_size = city_text.get_size()
city_text_rect = (
(self.width - city_text_size[0]) / 2.0,
0,
city_text_size[0],
city_text_size[1]
)
self.surface.blit(city_text, city_text_rect)
if self._current_conditions:
conditions_text = self._render_conditions()
conditions_text_size = conditions_text.get_size()
conditions_text_rect = (
(self.width - conditions_text_size[0]) / 2.0,
self.height - conditions_text_size[1],
conditions_text_size[0],
conditions_text_size[1]
)
self.surface.blit(conditions_text, conditions_text_rect)
icon = self._render_icon()
has_icon = (icon is not None)
temp_text = self._render_temperature()
temp_text_size = temp_text.get_size()
if has_icon:
icon_size = icon.get_size()
icon_origin_x = (
(
self.width - (
icon_size[0] + self.ICON_SPACING +
temp_text_size[0]
)
) / 2.0
)
icon_origin_y = (
city_text_size[1] + (
self.height - conditions_text_size[1] -
city_text_size[1] - icon_size[1]
) / 2.0
)
icon_rect = (
icon_origin_x,
icon_origin_y,
icon_size[0],
icon_size[1]
)
self.surface.blit(icon, icon_rect)
temp_text_origin_y = (
city_text_size[1] + (
self.height - conditions_text_size[1] -
city_text_size[1] - temp_text_size[1]
) / 2.0
)
if has_icon:
temp_text_origin_x = (
icon_rect[0] + icon_size[0] +
self.ICON_SPACING
)
temp_text_rect = (
temp_text_origin_x,
temp_text_origin_y,
temp_text_size[0],
temp_text_size[1]
)
else:
temp_text_rect = (
(self.width - temp_text_size[0]) / 2.0,
temp_text_origin_y,
temp_text_size[0],
temp_text_size[1]
)
self.surface.blit(temp_text, temp_text_rect)
self._should_redraw = False

View File

View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014-2016 Tomek Wójcik <tomek@bthlabs.pl>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import ConfigParser
import imp
import os
import sys
import traceback
RET_OK = 0
RET_NO_ARGS = 1
RET_ERROR = 99
CONFIG_SECTION_PIE_TIME = 'PieTime'
CONFIG_SECTION_SDL = 'SDL'
SDL_DEFAULTS = {
'VIDEODRIVER': None
}
def _find_module(name, search_path):
import_path = name.split('.', 1)
mod_f, mod_path, mod_desc = imp.find_module(import_path[0], search_path)
if mod_desc[2] == imp.PKG_DIRECTORY:
return _find_module(
import_path[1], [os.path.abspath(mod_path)]
)
else:
return mod_f, mod_path, mod_desc
def main():
try:
config_file_path = sys.argv[1]
except IndexError:
print 'usage: %s [CONFIG_FILE]' % sys.argv[0]
return RET_NO_ARGS
config = ConfigParser.SafeConfigParser()
config.optionxform = str
config.read(config_file_path)
app_spec = config.get(CONFIG_SECTION_PIE_TIME, 'app_module', True)
try:
app_module, app_obj = app_spec.split(':')
except ValueError:
print "%s: failed to find application '%s'" % (
sys.argv[0], app_spec
)
return RET_ERROR
mod_f = None
result = RET_OK
try:
mod_search_path = [os.getcwd()] + sys.path
mod_f, mod_path, mod_desc = _find_module(app_module, mod_search_path)
mod = imp.load_module(app_module, mod_f, mod_path, mod_desc)
app = getattr(mod, app_obj)
if config.has_option(CONFIG_SECTION_PIE_TIME, 'log_path'):
app.log_path = config.get(CONFIG_SECTION_PIE_TIME, 'log_path')
sdl_config = dict(SDL_DEFAULTS)
if config.has_section(CONFIG_SECTION_SDL):
sdl_config.update({
x[0]: x[1] for x in config.items(CONFIG_SECTION_SDL)
})
for k, v in sdl_config.iteritems():
if v:
os.environ['SDL_%s' % k] = v
result = app.run(standalone=False)
except:
traceback.print_exc()
result = RET_ERROR
finally:
if mod_f:
mod_f.close()
return result
if __name__ == '__main__':
sys.exit(main())