Initial public release of PieTime!
\o/
This commit is contained in:
commit
0912fd15e8
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.swp
|
||||||
|
.pybuild/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
pie_time.egg-info/
|
33
CONTRIBUTING.md
Normal file
33
CONTRIBUTING.md
Normal 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
19
LICENSE
Normal 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
4
MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
include pie_time/cards/resources/*.ttf
|
||||||
|
include README.rst
|
||||||
|
include LICENSE
|
||||||
|
include requirements.txt
|
42
README.rst
Normal file
42
README.rst
Normal 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
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
_build/
|
177
docs/Makefile
Normal file
177
docs/Makefile
Normal 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
16
docs/api.rst
Normal 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
22
docs/builtin_cards.rst
Normal 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
261
docs/conf.py
Normal 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
104
docs/developer_guide.rst
Normal 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
69
docs/index.rst
Normal 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`
|
79
docs/requirements_and_installation.rst
Normal file
79
docs/requirements_and_installation.rst
Normal 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
295
docs/user_guide.rst
Normal 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
0
examples/__init__.py
Normal file
38
examples/basic_usage.py
Normal file
38
examples/basic_usage.py
Normal 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()
|
69
examples/customization_example.py
Normal file
69
examples/customization_example.py
Normal 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
3
examples/example.ini
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[PieTime]
|
||||||
|
app_module = examples.customization_example:app
|
||||||
|
log_path = log.txt
|
84
extra/initscript.debian
Normal file
84
extra/initscript.debian
Normal 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
|
7
extra/pie-time.ini.example
Normal file
7
extra/pie-time.ini.example
Normal 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
12
pie_time/__init__.py
Normal 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
511
pie_time/application.py
Normal 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
195
pie_time/card.py
Normal 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')
|
3
pie_time/cards/__init__.py
Normal file
3
pie_time/cards/__init__.py
Normal 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
154
pie_time/cards/clock.py
Normal 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
140
pie_time/cards/picture.py
Normal 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
|
BIN
pie_time/cards/resources/PTM55FT.ttf
Executable file
BIN
pie_time/cards/resources/PTM55FT.ttf
Executable file
Binary file not shown.
BIN
pie_time/cards/resources/linea-weather-10.ttf
Normal file
BIN
pie_time/cards/resources/linea-weather-10.ttf
Normal file
Binary file not shown.
BIN
pie_time/cards/resources/opensans-light.ttf
Executable file
BIN
pie_time/cards/resources/opensans-light.ttf
Executable file
Binary file not shown.
286
pie_time/cards/weather.py
Normal file
286
pie_time/cards/weather.py
Normal 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
|
0
pie_time/scripts/__init__.py
Normal file
0
pie_time/scripts/__init__.py
Normal file
104
pie_time/scripts/pie_time.py
Normal file
104
pie_time/scripts/pie_time.py
Normal 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
3
requirements-dev.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mock==1.3.0
|
||||||
|
nose==1.3.7
|
||||||
|
Sphinx==1.3.5
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pygame>=1.9.1
|
||||||
|
requests>=2.4.1
|
73
setup.py
Normal file
73
setup.py
Normal 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
|
||||||
|
}
|
||||||
|
)
|
89
tests/test_abstract_card.py
Normal file
89
tests/test_abstract_card.py
Normal 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
744
tests/test_application.py
Normal 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
229
tests/test_clock_card.py
Normal 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
266
tests/test_picture_card.py
Normal 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
697
tests/test_weather_card.py
Normal 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
|
Loading…
Reference in New Issue
Block a user