Initial public release of PieTime!

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

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.pyc
*.pyo
*.swp
.pybuild/
build/
dist/
pie_time.egg-info/

33
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,33 @@
# Quick guide to contributing to PieTime
This document describes the process of contributing to PieTime.
## Mailing List
The mailing list is the designated way of discussing anything related to
PieTime. Use it to report issues, submit patches etc.
Mailing list address: pietime@librelist.com
## Submitting patches
If you've made changes to PieTime source, you're welcome to submit a patch.
Before doing so, make sure you've updated tests and docs to reflect your
changes. Additionally, run the *pep8* utility on changed files.
Once you're done, create a patch from your changes and send it to the mailing
list along with a brief description. If your changes span over multiple
commits, please squash them into one before submitting the patch.
## License information
PieTime itself is licensed under the MIT license, so any code introduced by
your patch must be compatible with this license. Note that, if you don't
explicitly mark a piece of code with "alien" license, it'll automatically be
licensed under the MIT license.
If your patch contains 3rd party resources, make sure that their license(s)
allow for redistribution in open source projects.
If your patch contains 3rd party code and/or resources, make sure you update
the docs and ``debian/copyright`` file accordingly.

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
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.

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include pie_time/cards/resources/*.ttf
include README.rst
include LICENSE
include requirements.txt

42
README.rst Normal file
View File

@ -0,0 +1,42 @@
PieTime
=======
Desk clock for your Raspberry Pi.
About
-----
PieTime lets you turn your Raspberry Pi into a desk clock. It's written in
Python using PyGame framework.
PieTime comes with three modules that allow displaying the clock, weather and
set of pictures. These modules (also known as *cards*) are highly configurable,
so you can tweak them to better suit your needs.
Additionally, an API is provided for users who wish to write their own cards.
Read on for an example of a card.
Features
--------
With PieTime you can:
* Choose any of the three builtin cards.
* Configure display aspects (e.g. text color) of the cards.
* Define card visibility intervals.
* Set up screen blanking period for power saving.
* Use the card API to create your own cards.
Author, License and Attributions
--------------------------------
PieTime has been created and is developed by
`Tomek Wójcik <https://www.bthlabs.pl/>`_.
PieTime is licensed under the MIT License.
PieTime uses the following 3rd party code and resources:
* Open Sans font by Steve Matteson, (Apache License, Version 2.0),
* Linea Weather 1.0 font by Dario Ferrando (CC BY 4.0),
* PT Mono font by Paratype (SIL OFL, Version 1.1).

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_build/

177
docs/Makefile Normal file
View File

@ -0,0 +1,177 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PiClock.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PiClock.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/PiClock"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PiClock"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

16
docs/api.rst Normal file
View File

@ -0,0 +1,16 @@
API
===
This document describes the interfaces of PieTime.
Application
-----------
.. autoclass:: pie_time.PieTime
:members:
Abstract Card
-------------
.. autoclass:: pie_time.AbstractCard
:members:

22
docs/builtin_cards.rst Normal file
View File

@ -0,0 +1,22 @@
Builtin Cards
=============
This document describes the builtin cards.
ClockCard
---------
.. autoclass:: pie_time.cards.ClockCard
:members:
PictureCard
-----------
.. autoclass:: pie_time.cards.PictureCard
:members:
WeatherCard
-----------
.. autoclass:: pie_time.cards.WeatherCard
:members:

261
docs/conf.py Normal file
View File

@ -0,0 +1,261 @@
# -*- coding: utf-8 -*-
#
# PieTime documentation build configuration file, created by
# sphinx-quickstart on Tue Oct 21 16:34:31 2014.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'PieTime'
copyright = u'2014-2016, Tomek Wójcik'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'PieTimedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'PieTime.tex', u'PieTime Documentation',
u'Tomek Wójcik', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'pie_time', u'PieTime Documentation',
[u'Tomek Wójcik'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'PieTime', u'PieTime Documentation',
u'Tomek Wójcik', 'PieTime', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

104
docs/developer_guide.rst Normal file
View File

@ -0,0 +1,104 @@
Developer Guide
===============
This document provides guide to hacking and extending PieTime.
Setup
-----
To develop PieTime or cards, some additional setup is required. First, it's
recommended to use a virtual environment, to separate from OS Python and
extensions. Secondly, use ``requirements-dev.txt`` to install additional
modules and tools used in development.
Custom card example
-------------------
.. sourcecode:: python
from pie_time.card import AbstractCard
import pygame
class ExampleCard(AbstractCard):
def initialize(self):
self.sprite = pygame.surface.Surface((20, 20))
self.sprite.fill((255, 0, 0))
self.orig_sprite_rect = self.sprite.get_rect()
self.orig_speed = [2, 2]
self.sprite_rect = self.orig_sprite_rect
self.speed = self.orig_speed
def show(self):
self.sprite_rect = self.orig_sprite_rect
self.speed = self.orig_speed
def tick(self):
self.sprite_rect = self.sprite_rect.move(self.speed)
if self.sprite_rect.left < 0 or self.sprite_rect.right > self.width:
self.speed[0] = -self.speed[0]
if self.sprite_rect.top < 0 or self.sprite_rect.bottom > self.height:
self.speed[1] = -self.speed[1]
self.surface.fill(self.background_color)
self.surface.blit(self.sprite, self.sprite_rect)
This example shows how easy it is to create custom cards for use in PieTime
decks.
Start by creating a custom class that inherits from *AbstractCard*. Then
implement a few methods to make it display the information you need (in this
example, a red square moving on the screen). Having done that, you'll be ready to use your custom card in a deck.
Head on to :py:class:`pie_time.AbstractCard` documentation for more
information about PieTime card API.
Speed considerations
--------------------
Since PieTime targets the Raspberry Pi, it's important to keep speed in mind.
When developing cards, take care to do as little work as possible in the
:py:meth:`pie_time.AbstractCard.tick` method.
For example, WeatherCard redraws its surface only when weather conditions
change. By doing so, the CPU load is reduced because the only thing PieTime has to do is blit the surface to screen.
Always test the behavior of your card in low FPS. Remember, that PieTime
targets small GPIO-connected LCD screens. For many of them, 20 FPS will be the best refresh rate. If your card behaves badly in such conditions, users may refrain from using it.
Threading considerations
------------------------
Sometimes, it's required to perform background tasks during lifetime of the
card. API exposed by Python's builtin ``threading`` module should come in handy
is such situations.
Take WeatherCard as an example. Once every 10 minutes, it fetches current
conditions from the Internet. If you look into the card's code, there's
``_refresh_conditions`` method. It uses ``threading.Timer`` class to schedule
fetching of the conditions in a separate thread of control. This way, the
HTTP request (which may take some time depending on the network conditions)
won't cause the PieTime application to freeze.
As always with threads, you should be aware of the standard pitfalls - variable
access synchronization, error handling, the GIL. Also, spawning too many
threads will impact PieTime's performance. If you use threads in your card,
make sure to test it on the device to see how it impacts the load.
Handling events
---------------
In every iteration of the main loop, PieTime reads list of current events from
PyGame. This list is available for the cards to use through
:py:attr:`pie_time.PieTime.events`.
The application itself handles the following events:
* ``QUIT`` (e.g. SIGTERM) - if this event appears, the application quits.
* ``KEYDOWN`` on the key specified by :py:attr:`pie_time.PieTime.KEY_QUIT` -
quits the application,
* ``MOUSEBUTTONDOWN`` anywhere on the screen when it's blanked -
if click to unblank is enabled,
* ``MOUSEBUTTONDOWN`` in one of the regions used to manually switch bedween
cards.

69
docs/index.rst Normal file
View File

@ -0,0 +1,69 @@
PieTime
=======
Desk clock for your Raspberry Pi.
About
-----
PieTime lets you turn your Raspberry Pi into a desk clock. It's written in
Python using Pygame framework.
PieTime comes with three modules that allow displaying the clock, weather and
set of pictures. These modules (also known as *cards*) are highly configurable,
so you can tweak them to better suit your needs.
Additionally, an API is provided for users who wish to write their own cards.
Read on for an example of a card.
Features
--------
With PieTime you can:
* Choose any of the three builtin cards.
* Configure display aspects (e.g. text color) of the cards.
* Define card visibility intervals.
* Set up screen blanking period for power saving.
* Use the card API to create your own cards.
Author, License and Attributions
--------------------------------
PieTime has been created and is developed by
`Tomek Wójcik <https://www.bthlabs.pl/>`_.
PieTime is licensed under the MIT License.
PieTime uses the following 3rd party code and resources:
* Open Sans font by Steve Matteson, (Apache License, Version 2.0),
* Linea Weather 1.0 font by Dario Ferrando (CC BY 4.0),
* PT Mono font by Paratype (SIL OFL, Version 1.1).
Source code and issues
----------------------
Source code is available via
`public Git repository <https://git.bthlabs.pl/tomekwojcik/pie-time>`_.
If you wish to contribute to the project, see ``CONTRIBUTING.md`` for more
info.
Contents
--------
.. toctree::
:maxdepth: 2
requirements_and_installation
user_guide
builtin_cards
developer_guide
api
Indices and tables
------------------
* :ref:`genindex`
* :ref:`search`

View File

@ -0,0 +1,79 @@
.. _requirements_and_installation:
Requirements and installation
=============================
This document describes the PieTime requirements and installation process.
Requirements
------------
PieTime requires the following to work:
* Python 2.7,
* PyGame 1.9.1 (also tested with 1.9.2a0),
* requests 2.4.1 (should work with newer versions).
Installing on Raspbian Jessie
-----------------------------
If you're using Raspbian Jessie on your Raspberry Pi, you can install PieTime
using binary packages by following the guide below.
**Add PieTime APT repository**
Create the file /etc/apt/sources.list.d/pie-time.list and add the following
line:
.. sourcecode:: text
deb https://pie-time.bthlabs.pl/repos/apt/ raspbian-jessie main
**Import the repository signing key**
.. sourcecode:: console
$ wget --quiet -O - https://pie-time.bthlabs.pl/keys/apt.asc | sudo apt-key add -
**Update the package lists and install PieTime**
.. sourcecode:: console
$ sudo apt-get update
$ sudo apt-get install pie-time
Installing on other systems using PyPI
--------------------------------------
If you wish to install PieTime on system other than Raspbian Jessie (and
potentially on device other than Raspberry Pi), you can do so using the
PyPI package by using the guide below.
#. Install PyGame dependencies,
#. Run ``$ sudo pip install pie_time`` to install PieTime and its dependencies.
**NOTE**: The second step may require installing additional packages from the
system repository, depending on your current setup.
Installing from the source
--------------------------
In order to install PieTime, please follow the guide below. Note that this
guide assumes Unix-like OS and root access.
#. Install PyGame dependencies,
#. Clone the repository ``$ git clone https://git.bthlabs.pl/tomekwojcik/pie-time.git``,
#. Enter the PieTime directory: ``$ cd pie-time``,
#. Install PieTime: ``$ python setup.py install``.
**NOTE**: The fourth step may require installing additional packages from the
system repository, depending on your current setup.
Installing as non-root user
---------------------------
If you wish to install PieTime as non-root user, you can do so by installing
it from the PyPI package or source code using a virtual env.
To learn more about Python virtual envs, see the
`virtual env documentation <https://virtualenv.readthedocs.org/en/latest/>`_

295
docs/user_guide.rst Normal file
View File

@ -0,0 +1,295 @@
.. _user_guide:
User Guide
==========
This document provides guide to using PieTime.
The application script
----------------------
In order to use PieTime, you'll have to write a simple Python script to set
up and (optionally) start the application.
**Short example**
.. sourcecode:: python
#!/usr/bin/env python2.7
from datetime import timedelta
import os
import sys
if os.getcwd() not in sys.path:
sys.path.insert(0, os.getcwd())
from pie_time import PieTime
from pie_time.cards import ClockCard, PictureCard, WeatherCard
deck = [
ClockCard,
(
WeatherCard, 20, {
'api_key': 'Your OpenWeatherMap API KEY',
'city': 'Wroclaw,PL'
}
),
(
PictureCard, 10, {
'urls': [
'http://lorempixel.com/320/240/city',
'http://lorempixel.com/200/125/technics'
]
}
)
]
blanker_schedule = (
timedelta(hours=23), timedelta(hours=6)
)
app = PieTime(deck, blanker_schedule=blanker_schedule)
if __name__ == '__main__':
app.run()
This script sets up PieTime application with the following settings:
* Three cards,
* Clock card set up to display for 60 seconds,
* Weather card set up to fetch data for the city of Wrocław in Poland and
display for 20 seconds,
* Picture frame card set to display two separate images (fetched from the Net)
for 10 seconds each.
* Blanker schedule set up to blank the screen between 23:00 (11:00 PM) and
6:00 AM,
* Click to unblank interval set to 10 seconds.
The deck
--------
The first argument passed to :py:class:`pie_time.PieTime` constructor defines
the deck of cards to be displayed, along with additional information about
each of the cards.
Example deck could look like this:
.. sourcecode:: python
deck = [
ClockCard,
(ClockCard, 30),
(ClockCard, 30, {'text_color': (255, 0, 0)}),
]
The first item is just a card class. A card defined this way will display
for the duration defined by :py:attr:`pie_time.PieTime.CARD_INTERVAL` and
won't have any additional settings.
The second item is a tuple of card class and number. A card defined this
way will display for the specified number of seconds and won't have any
additional settings.
The third item is a tuple of card class, number and dictionary. A card
defined this way will display for the specified number of seconds and will
have additional settings as specified by the dictionary.
Blanker schedule
----------------
The *blanker_schedule* keyword argument defines how the screen should be
blanked.
Example blanker schedule could look like this:
.. sourcecode:: python
blanker_schedule = (
datetime.timedelta(hours=23), datetime.timedelta(hours=6)
)
Such a schedule will make the application blank the screen between 23:00
(11:00 PM) and 6:00 AM.
When the blanker is active, the screen is filled with color defined in
:py:attr:`pie_time.PieTime.BLANK_COLOR`.
Blanker also prevents the following actions:
* Transitioning cards,
* Calling the visible card's ``tick()`` method,
* Blitting the visible card's surface to the screen.
When the blanker deactivates it transitions to the first card from the deck.
Click to unblank
----------------
A PieTime application can be set up to allow temporary overriding of the screen
blanker. In order to do so, set *click_to_unblank_interval* to a non-negative
number. When the screen is blanked, just click anywhere and PieTime will show
for number of seconds defined by *click_to_unblank_interval*. Before
unblanking, PieTime will set the first card from deck as the current card.
Since PieTime uses PyGame, it'll automatically support many input devices. For
example, a properly configured touch screen for PiTFT-like displays should be
supported out of the box.
Click to transition
-------------------
If you wish, you can change the currently visible card manually. In order to do
so, just click (or tap) the bottom-left or bottom-right corner of the screen.
The bottom-left corner will switch to the previous card. The bottom-right
corner will switch to the next card.
This feature is enabled automatically and can be disabled by setting
*click_to_transition* keyword argument to ``False``.
Other settings
--------------
PieTime app allows changing other settings. Have a look at
:py:class:`pie_time.PieTime` class documentation to learn more.
Video drivers and screen size
-----------------------------
Since PieTime is based on PyGame, it supports the same range ouf output
devices. As of time of writing this document, PieTime has been tested with
``x11``, ``fbcon`` and ``Quartz`` video drivers.
You can configure the outut device using SDL environment variables. See the
`SDL environment variables documentation <https://www.libsdl.org/release/SDL-1.2.15/docs/html/sdlenvvars.html>`_ to learn more.
Since PieTime mostly targets LCD shields, the screen size defaults to
320x240px. Support for other screen sizes is limited.
Testing the script
------------------
Once you've created the script to set up the application and chosen the video
driver, you can start PieTime manually using the following command:
.. sourcecode:: console
$ python2.7 <path_to_app_script>
In this case, video driver will be chosen automatically. If you get any
errors, try using a different video driver. Note that framebuffer drivers
usually require root privileges.
To exit the application, press *[ESC]*.
**NOTE**: If you installed PieTime from the PyPI package or source code in a
virtual env, make sure it's properly activated before trying to start the
application.
Creating and using the PieTime INI file
---------------------------------------
After you've tested your application script and are satisfied with it, it's
time to create the INI file. This INI file will be used by the *pie_time*
program to set up and launch your application.
**Short example**
.. sourcecode:: ini
[PieTime]
app_module = examples.customization_example:app
log_path = log.txt
[SDL]
VIDEODRIVER = fbcon
**The PieTime section**
The *PieTime* section should contain the following fields:
* ``app_module`` (string) - app module import path,
* ``log_path`` (string) - optional path to log file (if omitted, standard
output will be used).
**The app module import path**
The *app_module* field defines the app module import path. This import path
will be used to import app script and extract the app object from it. In the
example, the import path translates to *app attribute in customization_example
module in examples package*. Note that the import path is related to the
current working directory.
**The SDL section**
The SDL section allows you to set up SDL environment variables before starting
the PieTime application. You can specify any environment variable supported by
SDL. The field names should be specified without the ``SDL_`` prefix, which
will be added automatically.
Starting PieTime on boot
------------------------
Since PieTime was designed as a desk clock replacement, it's best to have it
start automatically on boot. In order to do so, please follow instructions in
one of the subsections.
**Setting up PieTime installed from APT repository**
If you installed PieTime from APT repository, follow the guide below to set it
up to start on boot.
#. Edit ``/etc/default/pie-time`` and adjust its contents according to your
needs,
#. Place the INI file in the path specified in the ``/etc/default/pie-time``
file,
#. Place your application script in the path specified in the INI file,
#. Run ``$ sudo dpkg-reconfigure pie-time`` and answer *Yes* when it asks you
about starting on boot,
#. Either reboot the Raspberry Pi or run
``$ sudo /etc/init.d/pie-time restart`` to start PieTime.
**NOTE**: You can skip the third step, if you replied *Yes* to the question
when installing the PieTime package.
**The /etc/default/pie-time file**
The ``/etc/default/pie-time`` file contains minimum shell environment required
to properly start the *pie_time* program.
Supported environment variables:
* ``WORKDIR`` - path to working directory (which will be used to import the app
module). Defaults to ``/var/lib/pie-time``.
* ``INI_PATH`` - path to the INI file. Defaults to ``/etc/pie-time.ini``.
* ``USER`` - name of the user which will start the app. Defaults to ``root``.
**NOTE**: If you change the ``USER`` field to a non-root user, make sure it can
acess the selected output device and paths (most notably, the log path).
**Setting up PieTime installed from PyPI package or source code**
If you chose to install PieTime from PyPI package or source code and wish to
start it on boot, the recommended method is to use
`supervisor <http://supervisord.org/>`_ to achieve that.
**Example supervisor config for PieTime**
.. sourcecode:: text
[program:pie_time]
command=/usr/bin/python2.7 /home/pi/pie-time/app.py
numprocs=1
directory=/home/pi/pie-time
autostart=true
user=root
stdout_logfile=/home/pi/pie-time/log/stdout.log
stderr_logfile=/home/pi/pie-time/log/stderr.log
Troubleshooting
---------------
In case of any problems, it's recommend to set the *verbose* keyword to
argument to ``True`` and start the app again. In verbose mode, the app's logger
is set to ``DEBUG`` level (as opposed to ``INFO`` in non-verbose mode) and will
display a lot of useful debugging information.

0
examples/__init__.py Normal file
View File

38
examples/basic_usage.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python2.7
from datetime import timedelta
import os
import sys
if os.getcwd() not in sys.path:
sys.path.insert(0, os.getcwd())
from pie_time import PieTime
from pie_time.cards import ClockCard, PictureCard, WeatherCard
deck = [
ClockCard,
(
WeatherCard, 20, {
'api_key': 'Your OpenWeatherMap API KEY',
'city': 'Wroclaw,PL'
}
),
(
PictureCard, 10, {
'urls': [
'http://lorempixel.com/320/240/city',
'http://lorempixel.com/200/125/technics'
]
}
)
]
blanker_schedule = (
timedelta(hours=23), timedelta(hours=6)
)
app = PieTime(deck, blanker_schedule=blanker_schedule)
if __name__ == '__main__':
app.run()

View File

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
import os
import sys
import time
if os.getcwd() not in sys.path:
sys.path.insert(0, os.getcwd())
from pie_time import PieTime
from pie_time.cards import ClockCard
from pie_time.card import AbstractCard
import pygame
class ExampleCard(AbstractCard):
def initialize(self):
self.sprite = pygame.surface.Surface((20, 20))
self.sprite.fill((255, 0, 0))
self.orig_sprite_rect = self.sprite.get_rect()
self.orig_speed = [2, 2]
self.sprite_rect = self.orig_sprite_rect
self.speed = self.orig_speed
def show(self):
self.sprite_rect = self.orig_sprite_rect
self.speed = self.orig_speed
def tick(self):
self.sprite_rect = self.sprite_rect.move(self.speed)
if self.sprite_rect.left < 0 or self.sprite_rect.right > self.width:
self.speed[0] = -self.speed[0]
if self.sprite_rect.top < 0 or self.sprite_rect.bottom > self.height:
self.speed[1] = -self.speed[1]
self.surface.fill(self.background_color)
self.surface.blit(self.sprite, self.sprite_rect)
class CustomApp(PieTime):
def __init__(self, *args, **kwargs):
super(CustomApp, self).__init__(*args, **kwargs)
self._ctt_region_prev = pygame.Rect(0, 0, 160, 240)
self._ctt_region_next = pygame.Rect(160, 0, 160, 240)
def will_blank(self):
self.logger.debug('CustomApp.will_blank')
def will_unblank(self):
self.logger.debug('CustomApp.will_unblank')
deck = [
(ClockCard, 10),
(ExampleCard, 10),
]
app = CustomApp(
deck, verbose=True, fps=40,
blanker_schedule=(
timedelta(hours=23), timedelta(hours=6)
),
click_to_unblank_interval=10
)
if __name__ == '__main__':
app.run()

3
examples/example.ini Normal file
View File

@ -0,0 +1,3 @@
[PieTime]
app_module = examples.customization_example:app
log_path = log.txt

84
extra/initscript.debian Normal file
View File

@ -0,0 +1,84 @@
#!/bin/bash
### BEGIN INIT INFO
# Provides: pie-time
# Required-Start: $network
# Required-Stop: $network
# Should-Start: $local_fs
# Should-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Desk clock application for the Raspberry Pi
# Description: Desk clock application for the Raspberry Pi
### END INIT INFO
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON="/usr/bin/pie_time"
NAME="pie-time"
DESC="Desk clock application for the Raspberry Pi"
RUNDIR="/var/run/pie-time"
PIDFILE="/var/run/pie-time/pie-time.pid"
WORKDIR="/var/lib/pie-time"
CONFIG_PATH="/etc/pie-time.ini"
USER="root"
if [ -f "/etc/default/pie-time" ];then
. "/etc/default/pie-time"
fi
. /lib/lsb/init-functions
set -e
case "$1" in
start)
echo -n "Starting $DESC: "
mkdir -p $RUNDIR
chmod 755 $RUNDIR
chown $USER $RUNDIR
if start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER --chdir $WORKDIR -b -m --exec $DAEMON -- $CONFIG_PATH;then
echo "$NAME."
else
echo "failed"
fi
;;
stop)
echo -n "Stopping $DESC: "
if start-stop-daemon --stop --retry forever/TERM/1 --quiet --oknodo --pidfile $PIDFILE;then
echo "$NAME."
rm -f $PIDFILE
else
echo "failed"
fi
;;
restart|force-reload)
${0} stop
${0} start
;;
status)
STATUS="4"
start-stop-daemon --status --pidfile $PIDFILE || STATUS="$?"
case $STATUS in
0)
echo "$NAME: running"
;;
1|3)
echo "$NAME: not running"
;;
*)
echo "$NAME: unable to determine the status"
;;
esac
;;
*)
echo "Usage: /etc/init.d/$NAME {start|stop|restart|force-reload|status}" >&2
exit 1
;;
esac
exit 0

View File

@ -0,0 +1,7 @@
[PieTime]
app_module = pie_time_app:app
log_path = /var/log/pie-time/daemon.log
[SDL]
VIDEODRIVER = fbcon
FBDEV = /dev/fb0

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())

3
requirements-dev.txt Normal file
View File

@ -0,0 +1,3 @@
mock==1.3.0
nose==1.3.7
Sphinx==1.3.5

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pygame>=1.9.1
requests>=2.4.1

73
setup.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python2.7
# -*- 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 codecs
from setuptools import setup
import pie_time
with codecs.open('README.rst', 'r', 'utf-8') as desc_f:
long_description = desc_f.read()
with codecs.open('requirements.txt', 'r', 'utf-8') as requirements_f:
requirements = requirements_f.read().split('\n')
SCRIPTS = [
'pie_time = pie_time.scripts.pie_time:main'
]
DOWNLOAD_URL = (
'https://git.bthlabs.pl/tomekwojcik/pie-time/archive/v%s.tar.gz' %
pie_time.__version__
)
setup(
name="pie_time",
version=pie_time.__version__,
packages=[
'pie_time',
'pie_time.cards',
'pie_time.scripts'
],
include_package_data=True,
test_suite='nose.collector',
zip_safe=False,
platforms='any',
tests_require=[
'nose',
],
author=pie_time.__author__.encode('utf-8'),
author_email='tomek@bthlabs.pl',
maintainer=pie_time.__author__.encode('utf-8'),
maintainer_email='tomek@bthlabs.pl',
url='https://pie-time.bthlabs.pl/',
download_url=DOWNLOAD_URL,
description='Desk clock for your Raspberry Pi.',
long_description=long_description,
license='https://git.bthlabs.pl/tomekwojcik/pie-time/src/master/LICENSE',
classifiers=[],
install_requires=requirements,
entry_points={
'console_scripts': SCRIPTS
}
)

View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
import mock
import os
import pygame
from pie_time import application as app_module
from pie_time.application import PieTime
from pie_time.card import AbstractCard
class Test_AbstractCard(object):
def _dummy_card(self, **settings):
app = PieTime(None)
card = AbstractCard()
card.set_app(app)
card.set_settings(settings)
return card
def test_init(self):
card = AbstractCard()
assert card._app is None
assert card._settings == {}
assert card._surface is None
def test_set_app(self):
card = AbstractCard()
app = mock.Mock(spec=PieTime)
card.set_app(app)
assert card._app == app
def test_set_settings(self):
card = AbstractCard()
settings = {'spam': 'eggs'}
card.set_settings(settings)
assert card._settings == settings
def test_width(self):
card = self._dummy_card()
assert card.width == card._app.screen_size[0]
def test_height(self):
card = self._dummy_card()
assert card.height == card._app.screen_size[1]
def test_surface(self):
fake_surface = mock.Mock(spec=pygame.surface.Surface)
with mock.patch.object(app_module.pygame.surface, 'Surface',
return_value=fake_surface):
card = self._dummy_card()
assert card.surface == fake_surface
assert card._surface == card.surface
app_module.pygame.surface.Surface.assert_called_with((
card.width, card.height
))
def test_background_color(self):
card = self._dummy_card()
assert card.background_color == card._app.BACKGROUND_COLOR
def test_background_color_override(self):
card = self._dummy_card(background_color=(255, 255, 255))
assert card.background_color == (255, 255, 255)
def test_path_for_resource(self):
card = self._dummy_card()
spam_path = card.path_for_resource('spam')
assert os.path.isabs(spam_path)
assert card.RESOURCE_FOLDER in spam_path
assert spam_path.endswith('spam')
spam_eggs_path = card.path_for_resource('spam', folder='eggs')
assert os.path.isabs(spam_eggs_path)
assert card.RESOURCE_FOLDER in spam_eggs_path
assert 'eggs' in spam_eggs_path
assert spam_eggs_path.endswith('spam')
def test_tick(self):
try:
card = self._dummy_card()
card.tick()
except Exception as exc:
assert isinstance(exc, RuntimeError)
assert exc.args[0] == 'TODO'
else:
assert False, 'Nothing was raised :('

744
tests/test_application.py Normal file
View File

@ -0,0 +1,744 @@
# -*- coding: utf-8 -*-
import datetime
import logging
import os
import sys
import mock
import pygame
from pie_time import application as app_module
from pie_time.application import logging as app_logging
from pie_time.application import EVENT_QUIT, EVENT_CLICK_TO_UNBLANK,\
EVENT_CLICK_TO_PREV_CARD, EVENT_CLICK_TO_NEXT_CARD, RET_OK, RET_ERROR,\
PieTime
from pie_time.card import AbstractCard
class DummyCard(AbstractCard):
def tick(self):
pass
def initialize(self):
self._surface = mock.Mock(spec=pygame.surface.Surface)
class FakeTimer(object):
def __init__(self, step=1, start=None):
if start is None:
self.n = 0 - step
else:
self.n = start
self._step = step
def __call__(self):
self.n += self._step
return self.n
def _now(*args):
return datetime.datetime(*args)
class Test_Application(object):
def _dummy_deck(self):
return [mock.Mock(spec=DummyCard)]
def _dummy_blanker_schedule(self):
return (datetime.timedelta(hours=1), datetime.timedelta(hours=2))
def _mocked_app(self, **kwargs):
mocked_app = PieTime(self._dummy_deck(), **kwargs)
mocked_app.init_pygame = mock.Mock()
mocked_app.get_screen = mock.Mock(
side_effect=lambda: mock.Mock(spec=pygame.Surface)
)
mocked_app._should_blank = mock.Mock(return_value=False)
mocked_app._blank = mock.Mock()
mocked_app._unblank = mock.Mock()
mocked_app.fill_screen = mock.Mock()
mocked_app._clock = mock.Mock(spec=pygame.time.Clock)
mocked_app.destroy_cards = mock.Mock()
mocked_app.quit_pygame = mock.Mock()
mocked_app._start_clock = mock.Mock()
mocked_app._setup_output_stream = mock.Mock()
mocked_app._setup_logging = mock.Mock()
mocked_app._logger = mock.Mock(spec=logging.Logger)
def init_cards(*args, **kwargs):
PieTime.init_cards(mocked_app)
mocked_app.init_cards = mock.Mock(side_effect=init_cards)
def transition_cards(*args, **kwargs):
mocked_app._current_card_idx = 0
mocked_app._current_card_time = 0
mocked_app._transition_cards = mock.Mock(
side_effect=transition_cards
)
return mocked_app
def _make_app(self, *args, **kwargs):
new_app = PieTime(*args, **kwargs)
new_app._logger = mock.Mock(spec=logging.Logger)
return new_app
def test_init_default_settings(self):
deck = self._dummy_deck()
app = self._make_app(deck)
assert app._deck == deck
assert app.screen is None
assert app.screen_size == (320, 240)
assert app.events == []
assert app.log_path is None
assert app._fps == 20
assert app._verbose is False
assert app._blanker_schedule is None
assert app._click_to_unblank_interval is None
assert app._click_to_transition is True
assert app._clock is None
assert app._cards == []
assert app._is_blanked is False
assert app._current_card_idx is None
assert app._current_card_time is None
assert app._should_quit is False
assert len(app._internal_events) == 0
assert app._ctu_timer is None
assert app._output_stream is None
assert app._ctt_region_prev.x == 0
assert app._ctt_region_prev.y == 210
assert app._ctt_region_prev.width == 30
assert app._ctt_region_prev.height == 30
assert app._ctt_region_next.x == 290
assert app._ctt_region_next.y == 210
assert app._ctt_region_next.width == 30
assert app._ctt_region_next.height == 30
def test_init_override_settings(self):
deck = self._dummy_deck()
blanker_schedule = self._dummy_blanker_schedule()
app = self._make_app(
deck, screen_size=(640, 480), fps=60, verbose=True,
blanker_schedule=blanker_schedule,
click_to_unblank_interval=10, click_to_transition=False,
log_path='/path/to/log_file.txt'
)
assert app.screen_size == (640, 480)
assert app.log_path == '/path/to/log_file.txt'
assert app._fps == 60
assert app._verbose is True
assert app._blanker_schedule == blanker_schedule
assert app._click_to_unblank_interval == 10
assert app._click_to_transition is False
def test_should_blank_scheduled(self):
blanker_schedule = self._dummy_blanker_schedule()
app = self._make_app(self._dummy_deck())
assert app._should_blank(now=_now(2014, 10, 15, 9)) is False
app = self._make_app(
self._dummy_deck(), blanker_schedule=blanker_schedule
)
assert app._should_blank(now=_now(2014, 10, 15, 0, 0, 0)) is False
assert app._should_blank(now=_now(2014, 10, 15, 1, 0, 0)) is True
assert app._should_blank(now=_now(2014, 10, 15, 1, 30, 0)) is True
assert app._should_blank(now=_now(2014, 10, 15, 1, 59, 59)) is True
assert app._should_blank(now=_now(2014, 10, 15, 2, 0, 0)) is False
assert app._should_blank(now=_now(2014, 10, 15, 9, 0, 0)) is False
blanker_schedule = (
datetime.timedelta(hours=23), datetime.timedelta(hours=6)
)
app = self._make_app(
self._dummy_deck(), blanker_schedule=blanker_schedule
)
assert app._should_blank(now=_now(2014, 10, 15, 0, 0, 0)) is True
assert app._should_blank(now=_now(2014, 10, 15, 5, 59, 59)) is True
assert app._should_blank(now=_now(2014, 10, 15, 6, 0, 0)) is False
assert app._should_blank(now=_now(2014, 10, 15, 12, 0, 0)) is False
assert app._should_blank(now=_now(2014, 10, 15, 22, 59, 59)) is False
assert app._should_blank(now=_now(2014, 10, 15, 23, 0, 0)) is True
def test_should_blank_ctu(self):
ctu = 10
app = self._make_app(self._dummy_deck())
app._has_click_to_unblank_event = mock.Mock(return_value=True)
assert app._should_blank() is False
assert app._ctu_timer is None
app = self._make_app(
self._dummy_deck(),
blanker_schedule=self._dummy_blanker_schedule(),
click_to_unblank_interval=ctu
)
app._is_blanked = True
app._has_click_to_unblank_event = mock.Mock(return_value=True)
assert app._should_blank(now=_now(2014, 10, 15, 1, 0, 0)) is False
assert app._ctu_timer == ctu
app = self._make_app(
self._dummy_deck(),
blanker_schedule=self._dummy_blanker_schedule(),
click_to_unblank_interval=ctu
)
app._is_blanked = True
app._ctu_timer = ctu
app._has_click_to_unblank_event = mock.Mock(return_value=False)
app._clock = mock.Mock(spec=pygame.time.Clock)
app._clock.get_time = mock.Mock(return_value=1 * 1000)
assert app._should_blank(now=_now(2014, 10, 15, 1, 0, 0)) is False
assert app._ctu_timer == ctu - 1
app = self._make_app(
self._dummy_deck(),
blanker_schedule=self._dummy_blanker_schedule(),
click_to_unblank_interval=ctu
)
app._is_blanked = True
app._ctu_timer = ctu
app._has_click_to_unblank_event = mock.Mock(return_value=False)
app._clock = mock.Mock(spec=pygame.time.Clock)
app._clock.get_time = mock.Mock(return_value=ctu * 1000)
assert app._should_blank(now=_now(2014, 10, 15, 1, 0, 0)) is True
assert app._ctu_timer is None
def test_blank(self):
app = self._make_app(self._dummy_deck())
app.screen = mock.Mock(spec=pygame.surface.Surface)
app.will_blank = mock.Mock()
app._blank()
assert app._is_blanked is True
assert app.will_blank.called is True
app.screen.fill.assert_called_with(PieTime.BLANK_COLOR)
def test_unblank(self):
app = self._make_app(self._dummy_deck())
app.init_cards()
app._is_blanked = True
app.will_unblank = mock.Mock()
app._unblank()
assert app._is_blanked is False
assert app.will_unblank.called is True
assert app._cards[0][0].show.call_count == 1
def test_transition_cards(self):
deck = [
(mock.Mock(spec=DummyCard), 1),
(mock.Mock(spec=DummyCard), 1)
]
app = self._make_app(self._dummy_deck())
app.init_cards()
app._transition_cards()
assert app._current_card_idx == 0
assert not app._cards[0][0].hide.called
assert app._cards[0][0].show.call_count == 1
app = self._make_app(deck)
app._clock = mock.Mock(spec=pygame.time.Clock)
timer = FakeTimer(step=2, start=0)
app._clock.get_time = mock.Mock(
side_effect=lambda: timer() * 1000.0
)
app.init_cards()
app._current_card_idx = 0
app._current_card_time = 0
app._transition_cards()
assert app._current_card_idx == 1
assert app._cards[0][0].hide.call_count == 1
assert app._cards[1][0].show.call_count == 1
app._transition_cards()
assert app._current_card_idx == 0
assert app._cards[0][0].show.call_count == 1
assert app._cards[1][0].hide.call_count == 1
def test_transition_cards_forced(self):
deck = [
(mock.Mock(spec=DummyCard), 1),
(mock.Mock(spec=DummyCard), 1)
]
app = self._make_app(deck)
app._clock = mock.Mock(spec=pygame.time.Clock)
timer = FakeTimer(step=2, start=0)
app._clock.get_time = mock.Mock(
side_effect=lambda: timer()
)
app.init_cards()
app._current_card_idx = 0
app._current_card_time = 0
app._transition_cards(direction=1, force=True)
assert app._current_card_idx == 1
app._transition_cards(direction=1, force=True)
assert app._current_card_idx == 0
app._transition_cards(direction=-1, force=True)
assert app._current_card_idx == 1
app._transition_cards(direction=-1, force=True)
assert app._current_card_idx == 0
def test_get_events_quit_pygame(self):
new_event_get = mock.Mock(return_value=[
pygame.event.Event(pygame.QUIT)
])
app = self._make_app(self._dummy_deck())
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
app._get_events()
assert app._internal_events == set([EVENT_QUIT])
def test_get_events_quit_key(self):
new_event_get = mock.Mock(return_value=[
pygame.event.Event(pygame.KEYDOWN, key=PieTime.KEY_QUIT)
])
app = self._make_app(self._dummy_deck())
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
app._get_events()
assert app._internal_events == set([EVENT_QUIT])
def test_get_events_click_to_unblank(self):
new_event_get = mock.Mock(return_value=[
pygame.event.Event(pygame.MOUSEBUTTONDOWN, pos=(160, 120))
])
app = self._make_app(self._dummy_deck(), click_to_unblank_interval=10)
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
app._is_blanked = False
app._get_events()
assert app._internal_events == set()
app._is_blanked = True
app._get_events()
assert app._internal_events == set([EVENT_CLICK_TO_UNBLANK])
def test_get_events_click_to_prev_card(self):
new_event_get = mock.Mock(return_value=[
pygame.event.Event(pygame.MOUSEBUTTONDOWN, pos=(10, 230))
])
app = self._make_app(self._dummy_deck(), click_to_transition=True)
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
app._is_blanked = False
app._get_events()
assert app._internal_events == set([EVENT_CLICK_TO_PREV_CARD])
app._is_blanked = True
app._get_events()
assert EVENT_CLICK_TO_PREV_CARD not in app._internal_events
app._click_to_transition = False
app._is_blanked = False
app._get_events()
assert EVENT_CLICK_TO_PREV_CARD not in app._internal_events
def test_get_events_click_to_next_card(self):
new_event_get = mock.Mock(return_value=[
pygame.event.Event(pygame.MOUSEBUTTONDOWN, pos=(310, 230))
])
app = self._make_app(self._dummy_deck(), click_to_transition=True)
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
app._is_blanked = False
app._get_events()
assert app._internal_events == set([EVENT_CLICK_TO_NEXT_CARD])
app._is_blanked = True
app._get_events()
assert EVENT_CLICK_TO_NEXT_CARD not in app._internal_events
app._click_to_transition = False
app._is_blanked = False
app._get_events()
assert EVENT_CLICK_TO_NEXT_CARD not in app._internal_events
def test_get_events_other_events(self):
events = [
pygame.event.Event(pygame.MOUSEBUTTONDOWN, pos=(160, 120)),
pygame.event.Event(pygame.KEYDOWN, key=pygame.K_RETURN)
]
new_event_get = mock.Mock(return_value=events)
app = self._make_app(self._dummy_deck(), click_to_transition=True)
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
app._is_blanked = False
app._get_events()
assert app._internal_events == set()
assert app.events == events
def test_has_quit_event(self):
app = self._make_app(self._dummy_deck())
assert app._has_quit_event() is False
app._internal_events.add(EVENT_QUIT)
assert app._has_quit_event() is True
def test_has_click_to_unblank_event(self):
app = self._make_app(self._dummy_deck(), click_to_unblank_interval=10)
assert app._has_click_to_unblank_event() is False
app._internal_events.add(EVENT_CLICK_TO_UNBLANK)
assert app._has_click_to_unblank_event() is True
def test_start_clock(self):
app = self._make_app(self._dummy_deck())
fake_clock = mock.Mock(spec=pygame.time.Clock)
with mock.patch.object(app_module.pygame.time, 'Clock',
return_value=fake_clock):
app._start_clock()
assert app_module.pygame.time.Clock.called is True
assert app._clock == fake_clock
def test_setup_output_stream_no_log_path(self):
deck = self._dummy_deck()
with mock.patch.object(PieTime, '_STREAM_FACTORY'):
with mock.patch.object(PieTime, '_setup_logging'):
app = self._make_app(deck, log_path=None)
app._setup_output_stream()
assert app._output_stream == PieTime._DEFAULT_OUTPUT_STREAM
assert PieTime._STREAM_FACTORY.called is False
def test_setup_output_stream_with_log_path(self):
deck = self._dummy_deck()
fake_file = mock.Mock(spec=file)
with mock.patch.object(PieTime, '_STREAM_FACTORY', new=fake_file):
with mock.patch.object(PieTime, '_setup_logging'):
app = self._make_app(deck, log_path='/path/to/log_file.txt')
app._setup_output_stream()
PieTime._STREAM_FACTORY.assert_called_with(app.log_path, 'a')
assert app._output_stream != fake_file
def test_setup_logging_silent(self):
deck = self._dummy_deck()
fake_logger = mock.Mock(spec=logging.Logger)
fake_requests_logger = mock.Mock(spec=logging.Logger)
fake_handler = mock.Mock(spec=logging.StreamHandler)
fake_formatter = mock.Mock(spec=logging.Formatter)
fake_requests_logger.handlers = ['spam', 'eggs']
fake_requests_logger.removeHandler = mock.Mock()
def fake_getLogger(name):
if name == 'PieTime':
return fake_logger
elif name == 'requests':
return fake_requests_logger
else:
return None
with mock.patch.object(app_logging, 'getLogger',
side_effect=fake_getLogger):
with mock.patch.object(app_logging,
'StreamHandler', return_value=fake_handler):
with mock.patch.object(app_logging,
'Formatter',
return_value=fake_formatter):
app = self._make_app(deck, verbose=False)
app._output_stream = 'spam'
app._setup_logging()
fake_logger.setLevel.assert_called_with(logging.INFO)
fake_requests_logger.setLevel.assert_called_with(
logging.WARNING
)
app_logging.StreamHandler.assert_called_with(
app._output_stream
)
assert app_logging.Formatter.called is True
fake_handler.setFormatter.assert_called_with(
fake_formatter
)
fake_logger.addHandler.assert_called_with(fake_handler)
assert fake_requests_logger.removeHandler.call_count == 2
fake_requests_logger.removeHandler.assert_any_call('spam')
fake_requests_logger.removeHandler.assert_any_call('eggs')
fake_requests_logger.addHandler.assert_called_with(
fake_handler
)
def test_setup_logging_verbose(self):
deck = self._dummy_deck()
fake_logger = mock.Mock(spec=logging.Logger)
fake_requests_logger = mock.Mock(spec=logging.Logger)
fake_requests_logger.handlers = []
def fake_getLogger(name):
if name == 'PieTime':
return fake_logger
elif name == 'requests':
return fake_requests_logger
else:
return None
with mock.patch.object(app_logging, 'getLogger',
side_effect=fake_getLogger):
with mock.patch.object(app_logging, 'StreamHandler'):
with mock.patch.object(app_logging, 'Formatter'):
app = self._make_app(deck, verbose=True)
app._output_stream = 'spam'
app._setup_logging()
fake_logger.setLevel.assert_called_with(logging.DEBUG)
assert fake_requests_logger.setLevel.called is False
def test_logger(self):
app = self._make_app(self._dummy_deck())
assert app.logger is not None
def test_init_pygame(self):
app = self._make_app(self._dummy_deck())
with mock.patch.object(app_module.pygame, 'init'):
with mock.patch.object(app_module.pygame.mouse, 'set_visible'):
app.init_pygame()
assert app_module.pygame.init.called
app_module.pygame.mouse.set_visible.assert_called_with(False)
assert app._clock is None
def test_quit_pygame(self):
app = self._make_app(self._dummy_deck())
with mock.patch.object(app_module.pygame, 'quit'):
app.quit_pygame()
assert app_module.pygame.quit.called
assert app._clock is None
def test_init_cards(self):
deck = [
mock.Mock(spec=DummyCard),
(mock.Mock(spec=DummyCard), 10),
(mock.Mock(spec=DummyCard), 20, {'spam': 'eggs'})
]
app = self._make_app(deck)
app.init_cards()
assert len(app._cards) == 3
app._cards[0][0].set_app.assert_called_with(app)
app._cards[0][0].set_settings.assert_called_with({})
assert app._cards[0][0].initialize.called
assert not app._cards[0][0].show.called
assert app._cards[0][1] == PieTime.CARD_INTERVAL
app._cards[1][0].set_app.assert_called_with(app)
app._cards[1][0].set_settings.assert_called_with({})
assert app._cards[1][0].initialize.called
assert not app._cards[1][0].show.called
assert app._cards[1][1] == 10
app._cards[2][0].set_app.assert_called_with(app)
app._cards[2][0].set_settings.assert_called_with({'spam': 'eggs'})
assert app._cards[2][0].initialize.called
assert not app._cards[2][0].show.called
assert app._cards[2][1] == 20
assert app._current_card_idx is None
assert app._current_card_time is None
def test_destroy_cards(self):
with mock.patch.object(DummyCard, 'quit'):
app = self._make_app([DummyCard])
app.init_cards()
app.destroy_cards()
assert len(app._cards) == 0
assert DummyCard.quit.called
def test_get_screen(self):
with mock.patch.object(app_module.pygame.display, 'set_mode',
return_value='spam'):
app = self._make_app(self._dummy_deck())
screen = app.get_screen()
assert screen == 'spam'
app_module.pygame.display.set_mode.\
assert_called_with(app.screen_size)
def test_fill_screen(self):
app = self._make_app(self._dummy_deck())
app.screen = mock.Mock(pygame.Surface)
app.fill_screen()
app.screen.fill.assert_called_with(PieTime.BACKGROUND_COLOR)
def test_run(self):
app = self._mocked_app()
def new_start_clock(*args, **kwargs):
app._clock.tick = mock.Mock(side_effect=lambda x: app.quit())
app._start_clock = mock.Mock(side_effect=new_start_clock)
new_event_get = mock.Mock(return_value=[])
with mock.patch.object(app_module.sys, 'exit'):
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
with mock.patch.object(app_module.pygame.display, 'flip'):
app.run()
assert app._setup_output_stream.called is True
assert app._setup_logging.called is True
assert app.init_pygame.called
assert app.get_screen.called
assert app.screen is not None
assert app.init_cards.called
assert app._start_clock.called
assert app_module.pygame.event.get.called
assert app._should_blank.called
assert app._unblank.called
assert not app._blank.called
assert app._transition_cards.called
assert app._cards[0][0].tick.called
assert app.fill_screen.called
app.screen.blit.assert_called_with(
app._cards[0][0].surface,
(0, 0, app._cards[0][0].width, app._cards[0][0].height)
)
assert pygame.display.flip.called
app._clock.tick.assert_called_with(app._fps)
assert app.destroy_cards.called
assert app.quit_pygame.called
app_module.sys.exit.assert_called_with(RET_OK)
def test_run_handling_exception(self):
app = self._mocked_app()
app._clock.tick = mock.Mock(side_effect=RuntimeError('spam'))
new_event_get = mock.Mock(return_value=[])
with mock.patch.object(app_module.sys, 'exit'):
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
with mock.patch.object(app_module.pygame.display, 'flip'):
app.run()
sys.exit.assert_called_with(RET_ERROR)
def test_run_handling_quit_event(self):
app = self._mocked_app()
app._get_events = mock.Mock()
app._has_quit_event = mock.Mock(return_value=True)
with mock.patch.object(app_module.sys, 'exit'):
with mock.patch.object(app_module.pygame.display, 'flip'):
app.run()
sys.exit.assert_called_with(RET_OK)
def test_run_blanking(self):
app = self._mocked_app()
timer = FakeTimer()
def should_blank(*args, **kwargs):
return (timer.n % 2 == 1)
app._should_blank = mock.Mock(side_effect=should_blank)
def clock_tick(*args, **kwargs):
timer()
if timer.n == 3:
app.quit()
app._clock.tick = mock.Mock(side_effect=clock_tick)
new_event_get = mock.Mock(return_value=[])
with mock.patch.object(app_module.sys, 'exit'):
with mock.patch.object(app_module.pygame.event, 'get',
new=new_event_get):
with mock.patch.object(app_module.pygame.display, 'flip'):
app.run()
assert app._unblank.call_count == 2
assert app._blank.call_count == 2
def test_run_handling_click_to_prev_event(self):
app = self._mocked_app()
app._internal_events = set([EVENT_CLICK_TO_PREV_CARD])
def clock_tick(*args, **kwargs):
app.quit()
app._clock.tick = mock.Mock(side_effect=clock_tick)
app._get_events = mock.Mock()
with mock.patch.object(app_module.sys, 'exit'):
with mock.patch.object(app_module.pygame.display, 'flip'):
app.run()
app._transition_cards.assert_called_with(
direction=-1, force=True
)
def test_run_handling_click_to_next_event(self):
app = self._mocked_app()
app._internal_events = set([EVENT_CLICK_TO_PREV_CARD])
def clock_tick(*args, **kwargs):
app.quit()
app._clock.tick = mock.Mock(side_effect=clock_tick)
app._get_events = mock.Mock()
with mock.patch.object(app_module.sys, 'exit'):
with mock.patch.object(app_module.pygame.display, 'flip'):
app.run()
app._transition_cards.assert_called_with(
direction=-1, force=True
)
def test_quit(self):
app = self._mocked_app()
app.quit()
assert app._should_quit is True

229
tests/test_clock_card.py Normal file
View File

@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
import datetime
import mock
import pygame
from pie_time import application as app_module
from pie_time.application import PieTime
from pie_time.cards import ClockCard
class Test_ClockCard(object):
def _dummy_card(self, **settings):
app = PieTime(None, screen_size=(320, 240))
app.path_for_resource = mock.Mock(
side_effect=lambda resource: resource
)
card = ClockCard()
card.set_app(app)
card.set_settings(settings)
return card
def test_initialize(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card.initialize()
assert card._time_font is not None
assert card._date_font is not None
def test_render_time(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card.initialize()
now = datetime.datetime.now().replace(second=0)
card._render_time(now)
card._time_font.render.assert_called_with(
now.strftime(card.TIME_FORMAT), True, card.GREEN
)
now = datetime.datetime.now().replace(second=1)
card._render_time(now)
card._time_font.render.assert_called_with(
now.strftime(card.TIME_FORMAT).replace(':', ' '), True,
card.GREEN
)
def test_render_time_override_format(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
time_format = '%H:%M'
card = self._dummy_card(time_format=time_format)
card.initialize()
now = datetime.datetime.now().replace(second=0)
card._render_time(now)
card._time_font.render.assert_called_with(
now.strftime(time_format), True, card.GREEN
)
def test_render_time_override_color(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
time_color = (255, 255, 255)
card = self._dummy_card(time_color=time_color)
card.initialize()
now = datetime.datetime.now().replace(second=0)
card._render_time(now)
card._time_font.render.assert_called_with(
now.strftime(card.TIME_FORMAT), True, (255, 255, 255)
)
def test_render_time_override_blink(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card(time_blink=False)
card.initialize()
now = datetime.datetime.now().replace(second=1)
card._render_time(now)
card._time_font.render.assert_called_with(
now.strftime(card.TIME_FORMAT), True, card.GREEN
)
def test_render_date(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card.initialize()
now = datetime.datetime.now()
card._render_date(now)
card._date_font.render.assert_called_with(
now.strftime(card.DATE_FORMAT), True, card.GREEN
)
def test_render_date_override_format(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
date_format = '%Y-%M-%D'
card = self._dummy_card(date_format=date_format)
card.initialize()
now = datetime.datetime.now().replace(second=0)
card._render_date(now)
card._date_font.render.assert_called_with(
now.strftime(date_format), True, card.GREEN
)
def test_render_date_override_color(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
date_color = (255, 255, 255)
card = self._dummy_card(date_color=date_color)
card.initialize()
now = datetime.datetime.now().replace(second=0)
card._render_date(now)
card._date_font.render.assert_called_with(
now.strftime(card.DATE_FORMAT), True, (255, 255, 255)
)
def test_update_now(self):
now = datetime.datetime.now()
current_delta = 0
fake_datetime = mock.Mock(spec=datetime.datetime)
def fake_datetime_now(*args, **kwargs):
return now + datetime.timedelta(seconds=current_delta)
fake_datetime.now = mock.Mock(side_effect=fake_datetime_now)
with mock.patch.object(app_module.datetime, 'datetime',
new=fake_datetime):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card.initialize()
card._app = mock.Mock(spec=PieTime)
card._app._clock = mock.Mock(spec=pygame.time.Clock)
card._app._clock.get_time = mock.Mock(
side_effect=lambda: current_delta
)
assert card._update_now() is True
assert card._now == now
assert card._current_interval == 0
current_delta = 500
assert card._update_now() is False
assert card._now == now
assert card._current_interval == current_delta
current_delta = 530
assert card._update_now() is True
assert card._now > now
assert card._current_interval == 30
def test_show(self):
card = self._dummy_card()
card._now = datetime.datetime.now()
card.show()
assert card._now is None
def test_tick_redraw(self):
now = datetime.datetime.now()
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card.initialize()
card._now = now
card._surface = mock.Mock(spec=pygame.surface.Surface)
card._update_now = mock.Mock(return_value=True)
time_surface = mock.Mock(spec=pygame.surface.Surface)
time_surface.get_size = mock.Mock(return_value=(120, 80))
card._render_time = mock.Mock(return_value=time_surface)
date_surface = mock.Mock(spec=pygame.surface.Surface)
date_surface.get_size = mock.Mock(return_value=(200, 60))
card._render_date = mock.Mock(return_value=date_surface)
card.tick()
assert card._update_now.called
card._render_time.assert_called_with(now)
card._render_date.assert_called_with(now)
assert time_surface.get_size.called
assert date_surface.get_size.called
card.surface.fill.assert_called_with(card.background_color)
card.surface.blit.assert_any_call(
time_surface, (100, 50, 120, 80)
)
card.surface.blit.assert_any_call(
date_surface, (60, 130, 200, 60)
)
def test_tick_dont_redraw(self):
with mock.patch.object(app_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card.initialize()
card._surface = mock.Mock(spec=pygame.surface.Surface)
card._update_now = mock.Mock(return_value=False)
card._render_time = mock.Mock()
card._render_date = mock.Mock()
card.tick()
assert card._update_now.called
assert card._render_time.called is False
assert card._render_date.called is False
assert card.surface.fill.called is False
assert card.surface.blit.called is False

266
tests/test_picture_card.py Normal file
View File

@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
import cStringIO
import mock
import pygame
import requests
from pie_time.application import PieTime
from pie_time.cards import picture as card_module
from pie_time.cards import PictureCard
class Test_PictureCard(object):
def _dummy_card(self, **settings):
app = PieTime(None, screen_size=(320, 240))
app.screen = mock.Mock(spec=pygame.surface.Surface)
app._logger = mock.Mock()
app.path_for_resource = mock.Mock(
side_effect=lambda resource: resource
)
card = PictureCard()
card.set_app(app)
card.set_settings(settings)
return card
def _dummy_initialized_card(self, **settings):
card = self._dummy_card(**settings)
card._load_picture = mock.Mock(
side_effect=lambda x: mock.Mock(spec=pygame.surface.Surface)
)
card.initialize()
return card
def test_initialize(self):
urls = ['spam', 'eggs']
card = self._dummy_card(urls=urls)
card._load_picture = mock.Mock()
card.initialize()
assert len(card._pictures) == len(urls)
assert card._current_picture_idx is None
assert card._load_picture.call_count == len(urls)
card._load_picture.assert_any_call(urls[0])
card._load_picture.assert_any_call(urls[1])
def test_load_picture_file(self):
fake_image = mock.Mock(spec=pygame.surface.Surface)
with mock.patch.object(card_module.pygame.image, 'load',
return_value=fake_image):
card = self._dummy_card()
result = card._load_picture('file:///spam')
assert result == fake_image
card_module.pygame.image.load.assert_called_with('/spam')
def test_load_picture_file_load_error(self):
with mock.patch.object(card_module.pygame.image, 'load',
side_effect=RuntimeError('ERROR')):
card = self._dummy_card()
result = card._load_picture('file:///spam')
assert result is None
def test_load_picture_net(self):
fake_image = mock.Mock(spec=pygame.surface.Surface)
fake_response = mock.Mock(spec=requests.Response)
fake_response.status_code = 200
fake_response.headers = {'Content-Type': ''}
fake_response.content = 'HERE IMAGE DATA BE'
fake_stringio = mock.Mock(spec=cStringIO.StringIO)
with mock.patch.object(card_module.pygame.image, 'load',
return_value=fake_image):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module.cStringIO, 'StringIO',
return_value=fake_stringio):
url = 'http://spam.com/eggs'
card = self._dummy_card()
result = card._load_picture(url)
assert result == fake_image
card_module.requests.get.assert_called_with(url)
card_module.cStringIO.StringIO.assert_called_with(
fake_response.content
)
card_module.pygame.image.load.assert_called_with(
fake_stringio, 'picture.'
)
def test_load_picture_net_requests_error(self):
def _get(*args, **kwargs):
raise RuntimeError('ERROR')
new_get = mock.Mock(side_effect=_get)
with mock.patch.object(card_module.requests, 'get',
side_effect=RuntimeError('TODO')):
url = 'http://spam.com/eggs'
card = self._dummy_card()
result = card._load_picture(url)
assert result is None
def test_load_picture_net_bad_response(self):
fake_response = mock.Mock(spec=requests.Response)
fake_response.status_code = 404
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
url = 'http://spam.com/eggs'
card = self._dummy_card()
result = card._load_picture(url)
assert result is None
def test_load_picture_net_load_error(self):
fake_response = mock.Mock(spec=requests.Response)
fake_response.status_code = 200
fake_response.headers = {'Content-Type': ''}
fake_response.content = 'HERE IMAGE DATA BE'
with mock.patch.object(card_module.pygame.image, 'load',
side_effect=RuntimeError('ERROR')):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
url = 'http://spam.com/eggs'
card = self._dummy_card()
result = card._load_picture(url)
assert result is None
def test_load_picture_convert(self):
fake_image = mock.Mock(spec=pygame.surface.Surface)
fake_image.convert = mock.Mock(return_value=fake_image)
with mock.patch.object(card_module.pygame.image, 'load',
return_value=fake_image):
card = self._dummy_card()
result = card._load_picture('file:///spam.jpg')
assert result == fake_image
fake_image.convert.assert_called_with(card._app.screen)
def test_load_picture_convert_alpha(self):
fake_image = mock.Mock(spec=pygame.surface.Surface)
fake_image.convert_alpha = mock.Mock(return_value=fake_image)
with mock.patch.object(card_module.pygame.image, 'load',
return_value=fake_image):
card = self._dummy_card()
result = card._load_picture('file:///spam.png')
assert result == fake_image
fake_image.convert_alpha.assert_called_with(card._app.screen)
def test_show_no_pictures(self):
card = self._dummy_initialized_card(urls=[])
card.show()
assert card._current_picture_idx is None
assert card._should_redraw is True
def test_show_one_picture(self):
card = self._dummy_initialized_card(urls=['http://spam.com/eggs.png'])
card.show()
assert card._current_picture_idx == 0
assert card._should_redraw is True
card.show()
assert card._current_picture_idx == 0
assert card._should_redraw is True
def test_show_many_pictures(self):
card = self._dummy_initialized_card(
urls=[
'http://spam.com/eggs.png', 'http://spam.com/spam.png',
'http://spam.com/spameggs.png'
]
)
card.show()
assert card._current_picture_idx == 0
assert card._should_redraw is True
card.show()
assert card._current_picture_idx == 1
assert card._should_redraw is True
card.show()
assert card._current_picture_idx == 2
assert card._should_redraw is True
card.show()
assert card._current_picture_idx == 0
assert card._should_redraw is True
def test_tick_no_pictures(self):
card = self._dummy_initialized_card(urls=[])
card._surface = mock.Mock(spec=pygame.surface.Surface)
card.show()
card.tick()
assert card._should_redraw is False
card.surface.fill.assert_called_with(card.background_color)
def test_tick_with_pictures(self):
card = self._dummy_initialized_card(
urls=['http://spam.com/eggs.png', 'http://spam.com/eggs.png']
)
card._surface = mock.Mock(spec=pygame.surface.Surface)
card._pictures[0].get_size = mock.Mock(return_value=(320, 240))
card._pictures[0].get_rect = mock.Mock(
return_value=(0, 0, 320, 240)
)
card._pictures[1].get_size = mock.Mock(return_value=(240, 180))
card._pictures[1].get_rect = mock.Mock(
return_value=(0, 0, 240, 180)
)
card.show()
card.tick()
assert card._should_redraw is False
card.surface.fill.assert_called_with(card.background_color)
card.surface.blit.assert_called_with(
card._pictures[0], (0, 0, 320, 240)
)
card.show()
card.tick()
assert card._should_redraw is False
card.surface.fill.assert_called_with(card.background_color)
card.surface.blit.assert_called_with(
card._pictures[1], (40, 30, 240, 180)
)
def test_tick_dont_redraw(self):
card = self._dummy_initialized_card(
urls=['http://spam.com/eggs.png', 'http://spam.com/eggs.png']
)
card._surface = mock.Mock(spec=pygame.surface.Surface)
card.show()
card._should_redraw = False
card.tick()
assert card._should_redraw is False
assert card.surface.fill.called is False
assert card.surface.blit.called is False

697
tests/test_weather_card.py Normal file
View File

@ -0,0 +1,697 @@
# -*- coding: utf-8 -*-
import threading
import mock
import pygame
import requests
from pie_time.application import PieTime
from pie_time.cards import weather as card_module
class Test_WeatherCard(object):
def _dummy_card(self, city='Wroclaw,PL', api_key='API key', **settings):
app = PieTime(None, screen_size=(320, 240))
app._logger = mock.Mock()
app.path_for_resource = mock.Mock(
side_effect=lambda resource: resource
)
card = card_module.WeatherCard()
card.set_app(app)
if city:
settings['city'] = city
if api_key:
settings['api_key'] = api_key
card.set_settings(settings)
return card
def _ok_payload(self):
return {
"base": "cmc stations",
"clouds": {
"all": 20
},
"cod": 200,
"coord": {
"lat": 51.1,
"lon": 17.03
},
"dt": 1413815400,
"id": 3081368,
"main": {
"humidity": 59,
"pressure": 1013,
"temp": 17,
"temp_max": 17,
"temp_min": 17
},
"name": "Wroclaw",
"sys": {
"country": "PL",
"id": 5375,
"message": 0.1316,
"sunrise": 1413782666,
"sunset": 1413820124,
"type": 1
},
"weather": [
{
"description": "few clouds",
"icon": "02d",
"id": 801,
"main": "Clouds"
}
],
"wind": {
"deg": 280,
"speed": 6.2
}
}
def test_initialize_no_city(self):
card = self._dummy_card(city=None)
try:
card.initialize()
except Exception as exc:
assert isinstance(exc, AssertionError)
assert exc.args[0] == 'Configuration error: missing city'
else:
assert False, 'Nothing was raised :('
def test_initialize_no_api_key(self):
card = self._dummy_card(api_key=None)
try:
card.initialize()
except Exception as exc:
assert isinstance(exc, AssertionError)
assert exc.args[0] == 'Configuration error: missing API key'
else:
assert False, 'Nothing was raised :('
def test_initialize(self):
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card = self._dummy_card()
card._refresh_conditions = mock.Mock()
card.initialize()
assert card._text_font is not None
assert card._temp_font is not None
assert card._icon_font is not None
assert card._timer is None
assert card._current_conditions is None
assert card._refresh_conditions.called
def test_refresh_conditions_requests_error(self):
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
side_effect=RuntimeError('TODO')):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions is None
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_refresh_conditions_bad_status_code(self):
fake_response = mock.Mock(spec=requests.Response)
fake_response.status_code = 404
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions is None
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_refresh_conditions_missing_weather_object(self):
payload = self._ok_payload()
payload['weather'] = []
fake_response = mock.Mock(spec=requests.Response)
fake_response.json = mock.Mock(return_value=payload)
fake_response.status_code = 200
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions is None
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_refresh_conditions_missing_conditions(self):
payload = self._ok_payload()
payload['weather'][0].pop('main')
fake_response = mock.Mock(spec=requests.Response)
fake_response.json = mock.Mock(return_value=payload)
fake_response.status_code = 200
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions is None
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_refresh_conditions_missing_temp(self):
payload = self._ok_payload()
payload['main'].pop('temp')
fake_response = mock.Mock(spec=requests.Response)
fake_response.json = mock.Mock(return_value=payload)
fake_response.status_code = 200
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions is None
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_refresh_conditions(self):
payload = self._ok_payload()
fake_response = mock.Mock(spec=requests.Response)
fake_response.json = mock.Mock(return_value=payload)
fake_response.status_code = 200
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions == {
'conditions': payload['weather'][0]['main'],
'icon': payload['weather'][0]['icon'],
'temperature': payload['main']['temp'],
}
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_refresh_conditions_no_icon(self):
payload = self._ok_payload()
payload['weather'][0].pop('icon')
fake_response = mock.Mock(spec=requests.Response)
fake_response.json = mock.Mock(return_value=payload)
fake_response.status_code = 200
fake_timer = mock.Mock(spec=threading.Timer)
fake_timer.start = mock.Mock()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
with mock.patch.object(card_module.requests, 'get',
return_value=fake_response):
with mock.patch.object(card_module, 'Timer',
return_value=fake_timer):
card = self._dummy_card()
card.initialize(refresh=False)
card._refresh_conditions()
assert card_module.requests.get.called
assert card._current_conditions == {
'conditions': payload['weather'][0]['main'],
'icon': None,
'temperature': payload['main']['temp'],
}
assert card._timer is not None
card_module.Timer.assert_called_with(
card.REFRESH_INTERVAL, card._refresh_conditions
)
assert card._timer.start.called
def test_render_city(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_city()
card._text_font.render.assert_called_with(
card._settings['city'], True, card.WHITE
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_city_override_color(self):
card = self._dummy_card(city_color=(255, 0, 0))
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_city()
card._text_font.render.assert_called_with(
card._settings['city'], True, (255, 0, 0)
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_conditions(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._current_conditions = {
'conditions': 'clouds'
}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_conditions()
card._text_font.render.assert_called_with(
'Clouds', True, card.WHITE
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_conditions_override_color(self):
card = self._dummy_card(conditions_color=(255, 0, 0))
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._current_conditions = {
'conditions': 'clouds'
}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_conditions()
card._text_font.render.assert_called_with(
'Clouds', True, (255, 0, 0)
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_icon(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
icon_code = '01d'
card._current_conditions = {'icon': icon_code}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_icon()
card._icon_font.render.assert_called_with(
card.WEATHER_CODE_TO_ICON[icon_code], True, card.WHITE
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_icon_override_color(self):
card = self._dummy_card(icon_color=(255, 0, 0))
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
icon_code = '01d'
card._current_conditions = {'icon': icon_code}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_icon()
card._icon_font.render.assert_called_with(
card.WEATHER_CODE_TO_ICON[icon_code], True, (255, 0, 0)
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_icon_no_icon(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
icon_code = None
card._current_conditions = {'icon': icon_code}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_icon()
assert card._icon_font.render.called is False
assert surface is None
def test_render_icon_unknown_icon(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
icon_code = 'spam'
card._current_conditions = {'icon': icon_code}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_icon()
assert card._icon_font.render.called is False
assert surface is None
def test_render_temperature(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._current_conditions = {
'temperature': 17
}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_temperature()
card._text_font.render.assert_called_with(
u'17°', True, card.WHITE
)
assert isinstance(surface, pygame.surface.Surface)
def test_render_temperature_override_color(self):
card = self._dummy_card(temperature_color=(255, 0, 0))
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._current_conditions = {
'temperature': 17
}
card._text_font.render = mock.Mock(
return_value=mock.Mock(spec=pygame.surface.Surface)
)
surface = card._render_temperature()
card._text_font.render.assert_called_with(
u'17°', True, (255, 0, 0)
)
assert isinstance(surface, pygame.surface.Surface)
def test_quit(self):
card = self._dummy_card()
card._timer = None
card.quit()
card = self._dummy_card()
card._timer = mock.Mock(spec=threading.Timer)
card._timer.cancel = mock.Mock()
card.quit()
assert card._timer.cancel.called
def test_tick_redraw(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._current_conditions = {
'conditions': 'Clouds',
'icon': '01d',
'temperature': 17
}
card._surface = mock.Mock(spec=pygame.surface.Surface)
city_surface = mock.Mock(spec=pygame.surface.Surface)
city_surface.get_size = mock.Mock(return_value=(120, 40))
card._render_city = mock.Mock(return_value=city_surface)
conditions_surface = mock.Mock(spec=pygame.surface.Surface)
conditions_surface.get_size = mock.Mock(return_value=(200, 40))
card._render_conditions = mock.Mock(
return_value=conditions_surface
)
icon_surface = mock.Mock(spec=pygame.surface.Surface)
icon_surface.get_size = mock.Mock(return_value=(128, 128))
card._render_icon = mock.Mock(return_value=icon_surface)
temperature_surface = mock.Mock(spec=pygame.surface.Surface)
temperature_surface.get_size = mock.Mock(return_value=(100, 82))
card._render_temperature = mock.Mock(
return_value=temperature_surface
)
card.tick()
card.surface.blit.assert_any_call(
city_surface, (100, 0, 120, 40)
)
card.surface.blit.assert_any_call(
conditions_surface, (60, 200, 200, 40)
)
card.surface.blit.assert_any_call(
icon_surface, (34, 56, 128, 128)
)
card.surface.blit.assert_any_call(
temperature_surface, (186, 79, 100, 82)
)
assert card._should_redraw is False
def test_tick_dont_redraw(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._should_redraw = False
card._surface = mock.Mock(spec=pygame.surface.Surface)
city_surface = mock.Mock(spec=pygame.surface.Surface)
city_surface.get_size = mock.Mock(return_value=(120, 40))
card._render_city = mock.Mock(return_value=city_surface)
conditions_surface = mock.Mock(spec=pygame.surface.Surface)
conditions_surface.get_size = mock.Mock(return_value=(200, 40))
card._render_conditions = mock.Mock(
return_value=conditions_surface
)
icon_surface = mock.Mock(spec=pygame.surface.Surface)
icon_surface.get_size = mock.Mock(return_value=(128, 128))
card._render_icon = mock.Mock(return_value=icon_surface)
temperature_surface = mock.Mock(spec=pygame.surface.Surface)
temperature_surface.get_size = mock.Mock(return_value=(100, 82))
card._render_temperature = mock.Mock(
return_value=temperature_surface
)
card.tick()
assert card._render_city.called is False
assert card._render_conditions.called is False
assert card._render_icon.called is False
assert card._render_temperature.called is False
assert card.surface.blit.called is False
assert card._should_redraw is False
def test_tick_without_icon(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._current_conditions = {
'conditions': 'Clouds',
'icon': '01d',
'temperature': 17
}
card._surface = mock.Mock(spec=pygame.surface.Surface)
city_surface = mock.Mock(spec=pygame.surface.Surface)
city_surface.get_size = mock.Mock(return_value=(120, 40))
card._render_city = mock.Mock(return_value=city_surface)
conditions_surface = mock.Mock(spec=pygame.surface.Surface)
conditions_surface.get_size = mock.Mock(return_value=(200, 40))
card._render_conditions = mock.Mock(
return_value=conditions_surface
)
card._render_icon = mock.Mock(return_value=None)
temperature_surface = mock.Mock(spec=pygame.surface.Surface)
temperature_surface.get_size = mock.Mock(return_value=(100, 82))
card._render_temperature = mock.Mock(
return_value=temperature_surface
)
card.tick()
card.surface.blit.assert_any_call(
city_surface, (100, 0, 120, 40)
)
card.surface.blit.assert_any_call(
conditions_surface, (60, 200, 200, 40)
)
card.surface.blit.assert_any_call(
temperature_surface, (110, 79, 100, 82)
)
assert card._should_redraw is False
def test_tick_without_conditions(self):
card = self._dummy_card()
with mock.patch.object(card_module.pygame.font, 'Font',
spec=pygame.font.Font):
card.initialize(refresh=False)
card._surface = mock.Mock(spec=pygame.surface.Surface)
city_surface = mock.Mock(spec=pygame.surface.Surface)
city_surface.get_size = mock.Mock(return_value=(120, 40))
card._render_city = mock.Mock(return_value=city_surface)
card._render_conditions = mock.Mock()
card._render_icon = mock.Mock()
card._render_temperature = mock.Mock()
card.tick()
assert card.surface.blit.call_count == 1
card.surface.blit.assert_called_with(
city_surface, (100, 0, 120, 40)
)
assert card._render_conditions.called is False
assert card._render_icon.called is False
assert card._render_temperature.called is False
assert card._should_redraw is False