├── .gitignore ├── LICENSE ├── README.md ├── base.kv ├── changelog ├── config.json ├── core ├── __init__.py ├── bgimage.py ├── bglabel.py ├── failedscreen.py ├── getplugins.py ├── hiddenbutton.py ├── infoscreen.py ├── webapi.py └── webinterface.py ├── images └── 10x10_transparent.png ├── infoscreen ├── main.py ├── screens ├── __init__.py ├── agenda │ ├── README │ ├── agenda.kv │ ├── authorise.py │ ├── conf.json │ └── screen.py ├── clock │ ├── clock.kv │ ├── conf.json │ ├── resources │ │ └── SFDigitalReadout-Medium.ttf │ └── screen.py ├── energenie │ ├── README │ ├── __init__.py │ ├── conf.json │ ├── energenie.kv │ ├── energenie_pigpio.py │ └── screen.py ├── finlandarrivals │ ├── README │ ├── conf.json │ ├── finlandarrivals.kv │ ├── finlandarrivals.py │ ├── resources │ │ └── SFDigitalReadout-Medium.ttf │ └── screen.py ├── football │ ├── README │ ├── __init__.py │ ├── conf.json │ ├── football.kv │ ├── footballresources │ │ ├── __init__.py │ │ └── footballscores.py │ ├── leagues.txt │ ├── screen.py │ ├── teams.txt │ └── web.py ├── isstracker │ ├── README │ ├── conf.json │ ├── images │ │ └── dot.png │ ├── iss_tle.json │ ├── isstracker.kv │ └── screen.py ├── londonbus │ ├── README │ ├── conf.json │ ├── londonbus.kv │ ├── londonbus.py │ └── screen.py ├── mythtv │ ├── README │ ├── cache │ │ └── cache.json │ ├── conf.json │ ├── mythtv.kv │ └── screen.py ├── photoalbum │ ├── README │ ├── conf.json │ ├── photos.kv │ └── screen.py ├── pong │ ├── README │ ├── conf.json │ ├── pong.kv │ └── screen.py ├── squeezeplayer │ ├── README │ ├── __init__.py │ ├── artworkresolver.py │ ├── conf.json │ ├── icons │ │ ├── sq_next.png │ │ ├── sq_pause.png │ │ ├── sq_play.png │ │ ├── sq_previous.png │ │ ├── sq_stop.png │ │ └── sq_volume.png │ ├── pylms │ │ ├── __init__.py │ │ ├── callback_server.py │ │ ├── client.py │ │ ├── player.py │ │ ├── server.py │ │ ├── tests.py │ │ └── utils.py │ ├── screen.py │ └── squeezeplayer.kv ├── template │ ├── README │ ├── conf.json │ ├── dummy.kv │ └── screen.py ├── tides │ ├── README │ ├── conf.json │ ├── images │ │ └── escalles.jpg │ ├── screen.py │ └── tides.kv ├── trains │ ├── README │ ├── conf.json │ ├── nationalrail.py │ ├── screen.py │ └── trains.kv ├── tube │ ├── conf.json │ ├── resources │ │ ├── __init__.py │ │ └── londonunderground.py │ ├── screen.py │ └── tube.kv ├── weather │ ├── README │ ├── conf.json │ ├── local.json │ ├── screen.py │ └── weather.kv ├── wordclock │ ├── README │ ├── conf.json │ ├── layouts │ │ ├── __init__.py │ │ ├── dutch.py │ │ ├── english.py │ │ ├── finnish.py │ │ ├── french.py │ │ ├── portuguese.py │ │ ├── spanish.py │ │ └── swedish.py │ ├── screen.py │ └── wordclock.kv └── xmas │ ├── README │ ├── conf.json │ ├── screen.py │ └── xmas.kv └── web └── templates ├── all_screens.tpl └── base.tpl /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom scripts for local testing/building 2 | copyscores.sh 3 | copypylms.sh 4 | *.NOTES 5 | cache/ 6 | 7 | # Agenda screen secret key 8 | screens/agenda/secret 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | env/ 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *,cover 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note: This project is no longer active development/maintenance so issues/pull requests may never get answered!** 2 | 3 | RPI-Info-Screen 4 | =============== 5 | 6 | Updated version of information screen. This branch is designed to work on the 7 | official Raspberry Pi display and so currently has a hard-coded resolution and also relies 8 | on touch screen for transition between screens. 9 | 10 | Installation 11 | ------------ 12 | 13 | First off, you need to install Kivy. I used the instructions [here](https://github.com/mrichardson23/rpi-kivy-screen/blob/master/README.md). Make sure you see them through, in particular the comment about making sure Kivy recognises the touch screen (you may need to run Kivy once to create the ini file). 14 | 15 | Two common dependencies for the screens are "[requests](https://www.raspberrypi.org/forums/viewtopic.php?f=91&t=79312#p563361)" and "[BeautifulSoup](https://www.howtoinstall.co/en/debian/wheezy/main/python-beautifulsoup/)" so you should install these too (see links). 16 | 17 | I'd recommend using git to clone the repository but you can just downloan the zip file and extract to a location of your choice. 18 | 19 | Unless you've got a good reason (e.g. testing new features) you should stick to the "Master" branch. 20 | 21 | Configuration 22 | ------------- 23 | 24 | Once you've downloaded (and extracted) the code you'll need to configure the screens. 25 | 26 | Each screen (in the "screens" folder) should have a conf.json file and a README which explains how to configure the screen for your needs. 27 | 28 | You can disable screens by changing the "enabled" parameter to "false" (without quotation marks). 29 | 30 | Running 31 | ------- 32 | 33 | I'd recommend testing the script before havng it start when your pi boots. So just run 34 | 35 | First make the file executable (if it isn't already). 36 | 37 | `chmod +x main.py` 38 | 39 | Now just run the file by typing 40 | 41 | `./main.py` 42 | 43 | If there are any errors with your screens, these should be indicated on the screen when you run it (this screen only displays on loading the software, once you browse away it won't be visible again). If you've got any unmet dependencies then you should exit (ctrl+c) and install these. 44 | 45 | Navigating screens 46 | ------------------ 47 | 48 | Navigation should be very straightforward, just touch the right or left edges of the screens to move to the next or previous screens. 49 | 50 | Where screens have multiple views (e.g. weather forecast for more than one location) then these are found by touching the top or bottom edges of the screen. 51 | 52 | Start on Boot 53 | ------------- 54 | 55 | If it's all working well and you want it to run when you boot your screen then you need to set up the init.d script. 56 | 57 | First, edit the infoscreen file and make sure the path to the script is correct. 58 | 59 | Next, move the screen to the init.d folder and make it executable. 60 | 61 | `sudo mv infoscreen /etc/init.d/` 62 | 63 | `sudo chmod +x /etc/init.d/infoscreen` 64 | 65 | And then, to install the script, run the following command: 66 | 67 | `sudo update-rc.d infoscreen defaults` 68 | 69 | Bug reporting 70 | ------------- 71 | 72 | Bugs can be reported in one of two locations: 73 | 74 | 1) The [Github issues page](https://github.com/elParaguayo/RPi-InfoScreen-Kivy/issues); or 75 | 76 | 2) The [project thread](https://www.raspberrypi.org/forums/viewtopic.php?f=41&t=121392) on the Raspberry Pi forum. 77 | 78 | Feature requests 79 | ---------------- 80 | 81 | Feature requests are very welcome and should be posted in either of the locations in the "Bug reporting" section. 82 | -------------------------------------------------------------------------------- /base.kv: -------------------------------------------------------------------------------- 1 | #:kivy 1.0.9 2 | 3 | scrmgr: iscreenmgr 4 | id: iscreen 5 | 6 | ScreenManager: 7 | id: iscreenmgr 8 | auto_bring_to_front: False 9 | 10 | HiddenButton: 11 | id: btn_left 12 | size: 50, root.height 13 | size_hint: None, None 14 | pos: root.width - 50, 0 15 | on_press: root.next_screen() 16 | 17 | HiddenButton: 18 | size: 50, root.height 19 | size_hint: None, None 20 | pos: 0, 0 21 | on_press: root.next_screen(True) 22 | 23 | 24 | padding: 2, 2 25 | canvas.before: 26 | Color: 27 | rgba: self.bgcolour 28 | Rectangle: 29 | size: self.size 30 | pos: self.pos 31 | 32 | 33 | canvas.before: 34 | Color: 35 | rgba: self.bgcolour 36 | Rectangle: 37 | size: self.size 38 | pos: self.pos 39 | canvas.after: 40 | Color: 41 | rgba: self.fgcolour 42 | Rectangle: 43 | size: self.size 44 | pos: self.pos 45 | -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | v0.4.1 Bugfix 2 | 3 | - Webserver bug fix 4 | 5 | v0.4 - Fourth VERSION 6 | 7 | New screens: 8 | - ISS Tracker 9 | - Helsinki Bus (thanks Karrika) 10 | - Pong 11 | - Christmas countdown 12 | 13 | New wordclock layouts: 14 | - French 15 | - Spanish 16 | - Portuguese 17 | - Finnish (thanks Karrika) 18 | - Swedish (thanks Karrika) 19 | 20 | Webserver: 21 | - Enable/disable screens from web browser 22 | - Configure screens from web browser 23 | - Screens can provide custom web pages 24 | 25 | 26 | v0.3.1 - Quick update 27 | 28 | New Screen: 29 | - Photo album slideshow 30 | 31 | 32 | v0.3 - Third VERSION 33 | 34 | New Screens: 35 | - Energenie control 36 | - Agenda (using Google calendars) 37 | 38 | 39 | v0.2.1 - SqueezePlayer bugfixes 40 | 41 | Bug Fixes: 42 | https://github.com/elParaguayo/RPi-InfoScreen-Kivy/issues/30 43 | https://github.com/elParaguayo/RPi-InfoScreen-Kivy/issues/31 44 | https://github.com/elParaguayo/RPi-InfoScreen-Kivy/issues/32 45 | https://github.com/elParaguayo/RPi-InfoScreen-Kivy/issues/33 46 | https://github.com/elParaguayo/RPi-InfoScreen-Kivy/issues/34 47 | 48 | New: 49 | Blurred background on SqueezePlayer screen to make controls look sharper. 50 | 51 | 52 | v0.2 - Second Version 53 | Bugfixes for v0.1 screens 54 | 55 | New Screens: 56 | - MythTV upcoming recordings (disabled by default) 57 | - SqueezePlayer control (disabled by default) 58 | - Wordclock 59 | 60 | Known issues: 61 | - Those weather images are still flashing... 62 | 63 | 64 | v0.1 - First version 65 | 66 | Known issues: 67 | - Flashing images on weather forecast screen. 68 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"webserver": { 2 | "enabled": true, 3 | "webport": 8088, 4 | "apiport": 8089 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/core/__init__.py -------------------------------------------------------------------------------- /core/bgimage.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.image import Image 2 | from kivy.properties import ListProperty 3 | from kivy.uix.behaviors import ButtonBehavior 4 | 5 | 6 | class BGImage(Image): 7 | """Custom widget to faciliate the ability to set background colour 8 | to an image via the "bgcolour" property. 9 | """ 10 | bgcolour = ListProperty([0, 0, 0, 0]) 11 | fgcolour = ListProperty([0, 0, 0, 0]) 12 | 13 | def __init__(self, **kwargs): 14 | super(BGImage, self).__init__(**kwargs) 15 | 16 | 17 | class BGImageButton(ButtonBehavior, BGImage): 18 | """Add button behaviour to the BGImage widget.""" 19 | pass 20 | -------------------------------------------------------------------------------- /core/bglabel.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.label import Label 2 | from kivy.properties import ListProperty 3 | from kivy.uix.behaviors import ButtonBehavior 4 | 5 | 6 | class BGLabel(Label): 7 | """Custom widget to faciliate the ability to set background colour 8 | to a label via the "bgcolour" property. 9 | """ 10 | bgcolour = ListProperty([0, 0, 0, 0]) 11 | 12 | def __init__(self, **kwargs): 13 | super(BGLabel, self).__init__(**kwargs) 14 | 15 | 16 | class BGLabelButton(ButtonBehavior, BGLabel): 17 | """Add button behaviour to the BGLabel widget.""" 18 | pass 19 | -------------------------------------------------------------------------------- /core/failedscreen.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.screenmanager import Screen 2 | from kivy.uix.label import Label 3 | 4 | 5 | class FailedScreen(Screen): 6 | """Custom Screen to notify users where certain plugins have not installed 7 | correctly. 8 | """ 9 | def __init__(self, **kwargs): 10 | super(FailedScreen, self).__init__(**kwargs) 11 | 12 | # Get details of the screens that have failed 13 | # Unmet dependencies 14 | self.dep = kwargs["dep"] 15 | # Other unhandled failures 16 | self.failed = kwargs["failed"] 17 | 18 | # Build our screen 19 | self.buildLabel() 20 | 21 | def buildLabel(self): 22 | mess = "One or more screens have not been initialised.\n\n" 23 | 24 | # Loop over screens with unmet dependencies 25 | if self.dep: 26 | mess += "The following screens have unmet dependencies:\n\n" 27 | for dep in self.dep: 28 | mess += "{0}: {1}\n".format(dep[0], ",".join(dep[1])) 29 | if self.failed: 30 | mess += "\n\n" 31 | 32 | # Loop over screens with other unhandled erros. 33 | if self.failed: 34 | mess += ("Errors were encountered trying to create the following " 35 | "screens:\n\n") 36 | for f in self.failed: 37 | mess += "{0}: {1}\n".format(*f) 38 | 39 | # Create a label. 40 | lbl = Label(text=mess) 41 | 42 | # Display it. 43 | self.add_widget(lbl) 44 | -------------------------------------------------------------------------------- /core/getplugins.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import json 4 | 5 | # Constants that are used to find plugins 6 | PluginFolder = "./screens" 7 | PluginScript = "screen.py" 8 | ScreenConf = "conf.json" 9 | 10 | 11 | def getPlugins(inactive=False): 12 | plugins = [] 13 | a = 1 14 | 15 | # Get the contents of the plugin folder 16 | possibleplugins = os.listdir(PluginFolder) 17 | 18 | # Loop over it 19 | for i in possibleplugins: 20 | location = os.path.join(PluginFolder, i) 21 | 22 | # Ignore anything that doesn't meet our criteria 23 | if (not os.path.isdir(location) or PluginScript not in 24 | os.listdir(location)): 25 | continue 26 | 27 | # Load the module info into a variavls 28 | inf = imp.find_module("screen", [location]) 29 | 30 | # Plugin needs a conf file. 31 | if ScreenConf in os.listdir(location): 32 | conf = json.load(open(os.path.join(location, ScreenConf))) 33 | 34 | # See if the user has disabled the plugin. 35 | if conf.get("enabled", False) or inactive: 36 | 37 | # Get the KV file text 38 | kvpath = os.path.join(location, conf["kv"]) 39 | kv = open(kvpath).readlines() 40 | 41 | # See if there's a web config file 42 | webfile = os.path.join(location, "web.py") 43 | if os.path.isfile(webfile): 44 | web = imp.find_module("web", [location]) 45 | else: 46 | web = None 47 | 48 | # Custom dict for the plugin 49 | plugin = {"name": i, 50 | "info": inf, 51 | "id": a, 52 | "screen": conf["screen"], 53 | "dependencies": conf.get("dependencies", list()), 54 | "kv": kv, 55 | "kvpath": kvpath, 56 | "params": conf.get("params", None), 57 | "enabled": conf.get("enabled", False), 58 | "web": web} 59 | 60 | plugins.append(plugin) 61 | a = a + 1 62 | 63 | # We're done so return the list of available/enabled plugins 64 | return plugins 65 | -------------------------------------------------------------------------------- /core/hiddenbutton.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.behaviors import ButtonBehavior 2 | from core.bglabel import BGLabel, BGLabelButton 3 | 4 | 5 | class HiddenButton(ButtonBehavior, BGLabel): 6 | pass 7 | -------------------------------------------------------------------------------- /core/infoscreen.py: -------------------------------------------------------------------------------- 1 | import imp 2 | 3 | from kivy.uix.floatlayout import FloatLayout 4 | from kivy.properties import BooleanProperty, ObjectProperty 5 | from kivy.lang import Builder 6 | from kivy.logger import Logger 7 | 8 | from core.failedscreen import FailedScreen 9 | from core.getplugins import getPlugins 10 | 11 | 12 | class InfoScreen(FloatLayout): 13 | # Flag for determining whether screen is locked or not 14 | locked = BooleanProperty(False) 15 | 16 | def __init__(self, **kwargs): 17 | scrmgr = ObjectProperty(None) 18 | 19 | super(InfoScreen, self).__init__(**kwargs) 20 | 21 | # Get our list of available plugins 22 | plugins = kwargs["plugins"] 23 | 24 | # We need a list to hold the names of the enabled screens 25 | self.availablescreens = [] 26 | 27 | # and an index so we can loop through them: 28 | self.index = 0 29 | 30 | # We want to handle failures gracefully so set up some variables 31 | # variable to hold the FailScreen object (if needed) 32 | self.failscreen = None 33 | 34 | # Empty lists to track various failures 35 | dep_fail = [] 36 | failedscreens = [] 37 | 38 | # Create a reference to the screenmanager instance 39 | # self.scrmgr = self.ids.iscreenmgr 40 | 41 | # Loop over plugins 42 | for p in plugins: 43 | 44 | # Set up a tuple to store list of unmet dependencies 45 | p_dep = (p["name"], []) 46 | 47 | # Until we hit a failure, there are no unmet dependencies 48 | unmet = False 49 | 50 | # Loop over dependencies and test if they exist 51 | for d in p["dependencies"]: 52 | try: 53 | imp.find_module(d) 54 | except ImportError: 55 | # We've got at least one unmet dependency for this screen 56 | unmet = True 57 | p_dep[1].append(d) 58 | Logger.error("Unmet dependencies " 59 | "for {} screen. Skipping...".format(p["name"])) 60 | 61 | # Can we use the screen? 62 | if unmet: 63 | # Add the tupe to our list of unmet dependencies 64 | dep_fail.append(p_dep) 65 | 66 | # No unmet dependencies so let's try to load the screen. 67 | else: 68 | try: 69 | plugin = imp.load_module("screen", *p["info"]) 70 | screen = getattr(plugin, p["screen"]) 71 | self.scrmgr.add_widget(screen(name=p["name"], 72 | master=self, 73 | params=p["params"])) 74 | Logger.info("Screen: {} loaded.".format(p["name"])) 75 | 76 | # Uh oh, something went wrong... 77 | except Exception, e: 78 | # Add the screen name and error message to our list 79 | Logger.error("Could not import " 80 | "{} screen. Skipping...".format(p["name"])) 81 | failedscreens.append((p["name"], repr(e))) 82 | 83 | else: 84 | # We can add the screen to our list of available screens. 85 | self.availablescreens.append(p["name"]) 86 | 87 | # If we've got any failures then let's notify the user. 88 | if dep_fail or failedscreens: 89 | 90 | # Create the FailedScreen instance 91 | self.failscreen = FailedScreen(dep=dep_fail, 92 | failed=failedscreens, 93 | name="FAILEDSCREENS") 94 | 95 | # Add it to our screen manager and make sure it's the first screen 96 | # the user sees. 97 | self.scrmgr.add_widget(self.failscreen) 98 | self.scrmgr.current = "FAILEDSCREENS" 99 | 100 | def toggle_lock(self, locked=None): 101 | if locked is None: 102 | self.locked = not self.locked 103 | else: 104 | self.locked = bool(locked) 105 | 106 | def reload_screen(self, screen): 107 | # Remove the old screen... 108 | self.remove_screen(screen) 109 | 110 | # ...and add it again. 111 | self.add_screen(screen) 112 | 113 | def add_screen(self, screenname): 114 | 115 | # Get the info we need to import this screen 116 | foundscreen = [p for p in getPlugins() if p["name"] == screenname] 117 | 118 | # Check we've found a screen and it's not already running 119 | if foundscreen and not screenname in self.availablescreens: 120 | 121 | # Get the details for the screen 122 | p = foundscreen[0] 123 | 124 | # Import it 125 | plugin = imp.load_module("screen", *p["info"]) 126 | 127 | # Get the reference to the screen class 128 | screen = getattr(plugin, p["screen"]) 129 | 130 | # Add the KV file to the builder 131 | Builder.load_file(p["kvpath"]) 132 | 133 | # Add the screen 134 | self.scrmgr.add_widget(screen(name=p["name"], 135 | master=self, 136 | params=p["params"])) 137 | 138 | # Add to our list of available screens 139 | self.availablescreens.append(screenname) 140 | 141 | # Activate screen 142 | self.switch_to(screename) 143 | 144 | elif screenname in self.availablescreens: 145 | 146 | # This shouldn't happen but we need this to prevent duplicates 147 | self.reload_screen(screenname) 148 | 149 | def remove_screen(self, screenname): 150 | 151 | # Get the list of screens 152 | foundscreen = [p for p in getPlugins(inactive=True) if p["name"] == screenname] 153 | 154 | # Loop over list of available screens 155 | while screenname in self.availablescreens: 156 | 157 | # Remove screen from list of available screens 158 | self.availablescreens.remove(screenname) 159 | 160 | # Change the display to the next screen 161 | self.next_screen() 162 | 163 | # Find the screen in the screen manager 164 | c = self.scrmgr.get_screen(screenname) 165 | 166 | # Call its "unload" method: 167 | if hasattr(c, "unload"): 168 | c.unload() 169 | 170 | # Delete the screen 171 | self.scrmgr.remove_widget(c) 172 | del c 173 | 174 | try: 175 | # Remove the KV file from our builder 176 | Builder.unload_file(foundscreen[0]["kvpath"]) 177 | except IndexError: 178 | pass 179 | 180 | 181 | def next_screen(self, rev=False): 182 | if not self.locked: 183 | if rev: 184 | self.scrmgr.transition.direction = "right" 185 | inc = -1 186 | else: 187 | self.scrmgr.transition.direction = "left" 188 | inc = 1 189 | 190 | self.index = (self.index + inc) % len(self.availablescreens) 191 | self.scrmgr.current = self.availablescreens[self.index] 192 | 193 | 194 | def switch_to(self, screen): 195 | 196 | if screen in self.availablescreens: 197 | 198 | # Activate the screen 199 | self.scrmgr.current = screen 200 | 201 | # Update the screen index 202 | self.index = self.availablescreens.index(screen) 203 | -------------------------------------------------------------------------------- /core/webapi.py: -------------------------------------------------------------------------------- 1 | '''Web interface for the Raspberry Pi Information Screen. 2 | 3 | by elParaguayo 4 | 5 | This module defines the underlying API. 6 | 7 | Screens can define their own web pages for custom configuration by using 8 | methods available in this API. 9 | 10 | API format: 11 | 12 | [HOST]/api//configure 13 | GET: returns JSON format of user-configurable settings for screen 14 | POST: takes JSON format of updated configuration. 15 | 16 | [HOST]/api//enable 17 | GET: enable the selected screen 18 | 19 | [HOST]/api//disable 20 | GET: disable the selected screen 21 | 22 | [HOST]/api//view 23 | GET: change to screen 24 | 25 | 26 | API Response format: 27 | successful: 28 | {"status": "success", 29 | "data": [body of response]} 30 | 31 | unsuccessful: 32 | {"status": "error", 33 | "message": [Error message]} 34 | ''' 35 | 36 | from threading import Thread 37 | from time import sleep 38 | import os 39 | import json 40 | import imp 41 | 42 | from kivy.app import App 43 | 44 | from bottle import Bottle, template, request, response 45 | 46 | from getplugins import getPlugins 47 | 48 | class InfoScreenAPI(Bottle): 49 | def __init__(self, infoscreen, folder): 50 | super(InfoScreenAPI, self).__init__() 51 | 52 | # Get reference to base screen object so API server can 53 | # access methods 54 | self.infoscreen = infoscreen.base 55 | 56 | # Get the folder path so we can access config files 57 | self.folder = folder 58 | 59 | # Get the list of screens 60 | #self.process_plugins() 61 | 62 | # Define our routes 63 | self.route("/", callback=self.default) 64 | self.error_handler[404] = self.unknown 65 | 66 | # API METHODS 67 | self.route("/api//configure", 68 | callback=self.get_config, 69 | method="GET") 70 | self.route("/api//configure", 71 | callback=self.set_config, 72 | method="POST") 73 | self.route("/api//enable", 74 | callback=self.enable_screen) 75 | self.route("/api//disable", 76 | callback=self.disable_screen) 77 | self.route("/api//view", 78 | callback=self.view) 79 | 80 | def api_success(self, data): 81 | """Base method for response to successful API calls.""" 82 | 83 | return {"status": "success", 84 | "data": data} 85 | 86 | def api_error(self, message): 87 | """Base method for response to unsuccessful API calls.""" 88 | 89 | return {"status": "error", 90 | "message": message} 91 | 92 | def get_config(self, screen): 93 | """Method to retrieve config file for screen.""" 94 | 95 | # Define the path to the config file 96 | conffile = os.path.join(self.folder, "screens", screen, "conf.json") 97 | 98 | if os.path.isfile(conffile): 99 | 100 | # Get the config file 101 | with open(conffile, "r") as cfg_file: 102 | 103 | # Load the JSON object 104 | conf = json.load(cfg_file) 105 | 106 | # Return the "params" section 107 | result = self.api_success(conf.get("params", dict())) 108 | 109 | else: 110 | 111 | # Something's gone wrong 112 | result = self.api_error("No screen called: {}".format(screen)) 113 | 114 | # Provide the response 115 | return json.dumps(result) 116 | 117 | def set_config(self, screen): 118 | 119 | try: 120 | # Get JSON data 121 | js = request.json 122 | 123 | if js is None: 124 | # No data, so provide error 125 | return self.api_error("No JSON data received. " 126 | "Check headers are set correctly.") 127 | 128 | else: 129 | # Try to save the new config 130 | success = self.save_config(screen, js) 131 | 132 | # If successfully saved... 133 | if success: 134 | 135 | # Reload the screen with the new config 136 | self.infoscreen.reload_screen(screen) 137 | 138 | # Provide success notification 139 | return self.api_success(json.dumps(js)) 140 | 141 | else: 142 | # We couldn't save new config 143 | return self.api_error("Unable to save configuration.") 144 | 145 | except: 146 | # Something's gone wrong 147 | return self.api_error("Invalid data received.") 148 | 149 | def default(self): 150 | # Generic response for unknown requests 151 | result = self.api_error("Invalid method.") 152 | return json.dumps(result) 153 | 154 | def unknown(self, addr): 155 | return self.default() 156 | 157 | def view(self, screen): 158 | try: 159 | self.infoscreen.switch_to(screen) 160 | return self.api_success("Changed screen to: {}".format(screen)) 161 | except: 162 | return self.api_error("Could not change screen.") 163 | 164 | 165 | # Helper Methods ########################################################### 166 | 167 | def save_config(self, screen, params): 168 | try: 169 | conffile = os.path.join(self.folder, "screens", screen, "conf.json") 170 | conf = json.load(open(conffile, "r")) 171 | conf["params"] = params 172 | with open(conffile, "w") as config: 173 | json.dump(conf, config, indent=4) 174 | return True 175 | except: 176 | return False 177 | 178 | def enable_screen(self, screen): 179 | try: 180 | # Update status in config 181 | self.change_screen_state(screen, True) 182 | 183 | # Make sure the screen is added 184 | self.infoscreen.add_screen(screen) 185 | 186 | # Success! 187 | return self.api_success("{} screen enabled.".format(screen)) 188 | 189 | except: 190 | 191 | # Something went wrong 192 | return self.api_error("Could not enable {} screen.".format(screen)) 193 | 194 | def disable_screen(self, screen): 195 | try: 196 | # Update status in config 197 | self.change_screen_state(screen, False) 198 | 199 | # Make sure the screen is added 200 | self.infoscreen.remove_screen(screen) 201 | 202 | # Success! 203 | return self.api_success("{} screen disabled.".format(screen)) 204 | except: 205 | 206 | # Something went wrong! 207 | return self.api_error("Could not disable {} screen.".format(screen)) 208 | 209 | def change_screen_state(self, screen, enabled): 210 | 211 | # Build path to config 212 | conffile = os.path.join(self.folder, "screens", screen, "conf.json") 213 | 214 | # Load existing config 215 | with open(conffile, "r") as f_config: 216 | conf = json.load(f_config) 217 | 218 | # Change status to desired state 219 | conf["enabled"] = enabled 220 | 221 | # Save the updated config 222 | with open(conffile, "w") as f_config: 223 | json.dump(conf, f_config, indent=4) 224 | -------------------------------------------------------------------------------- /images/10x10_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/images/10x10_transparent.png -------------------------------------------------------------------------------- /infoscreen: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: myservice 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Put a short description of the service here 10 | # Description: Put a long description of the service here 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=/home/pi/RPi-InfoScreen-Kivy 15 | DAEMON=$DIR/main.py 16 | DAEMON_NAME="RPi Information Screen" 17 | 18 | # Add any command line options for your daemon here 19 | DAEMON_OPTS="" 20 | 21 | # This next line determines what user the script runs as. 22 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python 23 | # (unless you use the pigpiod module). 24 | DAEMON_USER=pi 25 | 26 | # The process ID of the script when it runs is stored here: 27 | PIDFILE=/var/run/$DAEMON_NAME.pid 28 | 29 | . /lib/lsb/init-functions 30 | 31 | do_start () { 32 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 33 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS 34 | log_end_msg $? 35 | } 36 | do_stop () { 37 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 38 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 39 | log_end_msg $? 40 | } 41 | 42 | case "$1" in 43 | 44 | start|stop) 45 | do_${1} 46 | ;; 47 | 48 | restart|reload|force-reload) 49 | do_stop 50 | do_start 51 | ;; 52 | 53 | status) 54 | status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? 55 | ;; 56 | 57 | *) 58 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 59 | exit 1 60 | ;; 61 | 62 | esac 63 | exit 0 64 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import json 5 | 6 | from kivy.app import App 7 | from kivy.core.window import Window 8 | from kivy.graphics import Rectangle, Color 9 | from kivy.lang import Builder 10 | from kivy.logger import Logger 11 | from kivy.properties import ListProperty, StringProperty 12 | from kivy.uix.screenmanager import ScreenManager, Screen 13 | from kivy.uix.behaviors import ButtonBehavior 14 | from kivy.uix.floatlayout import FloatLayout 15 | from kivy.uix.boxlayout import BoxLayout 16 | from kivy.uix.label import Label 17 | from kivy.uix.scrollview import ScrollView 18 | 19 | from core.bglabel import BGLabel, BGLabelButton 20 | from core.getplugins import getPlugins 21 | from core.hiddenbutton import HiddenButton 22 | from core.infoscreen import InfoScreen 23 | 24 | # Set the current working directory 25 | os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) 26 | 27 | VERSION = "0.4.1" 28 | 29 | 30 | class InfoScreenApp(App): 31 | base = None 32 | def build(self): 33 | # Window size is hardcoded for resolution of official Raspberry Pi 34 | # display. Can be altered but plugins may not display correctly. 35 | Window.size = (800, 480) 36 | self.base = InfoScreen(plugins=plugins) 37 | return self.base 38 | 39 | if __name__ == "__main__": 40 | # Load our config 41 | with open("config.json", "r") as cfg_file: 42 | config = json.load(cfg_file) 43 | 44 | # Get a list of installed plugins 45 | plugins = getPlugins() 46 | 47 | # Get the base KV language file for the Info Screen app. 48 | kv_text = "".join(open("base.kv").readlines()) + "\n" 49 | 50 | # Load the master KV file 51 | # Builder.load_string(kv_text) 52 | Builder.load_file("base.kv") 53 | 54 | # Loop over the plugins 55 | for p in plugins: 56 | 57 | # and add their custom KV files to create one master KV file 58 | # kv_text += "".join(p["kv"]) 59 | Builder.load_file(p["kvpath"]) 60 | 61 | # Do we want a webserver? 62 | web = config.get("webserver", dict()) 63 | 64 | # Is bottle installed? 65 | try: 66 | 67 | # I don't like doing it this way (all imports should be at the top) 68 | # but I'm feeling lazy 69 | from core.webinterface import start_web_server 70 | web_enabled = True 71 | 72 | except ImportError: 73 | Logger.warning("Bottle module not found. Cannot start webserver.") 74 | web_enabled = False 75 | 76 | if web.get("enabled") and web_enabled: 77 | 78 | # Start our webserver 79 | webport = web.get("webport", 8088) 80 | apiport = web.get("apiport", 8089) 81 | debug = web.get("debug", False) 82 | start_web_server(os.path.dirname(os.path.abspath(__file__)), 83 | webport, 84 | apiport, 85 | debug) 86 | 87 | # Good to go. Let's start the app. 88 | InfoScreenApp().run() 89 | -------------------------------------------------------------------------------- /screens/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/__init__.py -------------------------------------------------------------------------------- /screens/agenda/README: -------------------------------------------------------------------------------- 1 | Agenda screen 2 | ------------- 3 | 4 | Requirements: 5 | - Currently this screen only works with Google calendars 6 | - dateutil and pytz python modules 7 | 8 | How to get it working 9 | --------------------- 10 | 11 | I won't lie, this is not the easiest screen to set up! The problem is that you need to authorise the script to use the Google API However, as this is an open source program, I'm not allowed to share my secret token. 12 | 13 | This means you have to create your own. To do this, follow steps 1 and 2 on this page: https://developers.google.com/google-apps/calendar/quickstart/python 14 | 15 | After step 1 you need to create a folder called "secret" in the "agenda" screen folder and save "client_secret.json" there. 16 | 17 | After step 2 you should run the authorise file once "python /path/to/RPi-InfoScreen-Kivy/screens/agenda/authorise.py". You should do this while in a desktop environment as this will open a browser to authorise your machine. 18 | 19 | Assuming that all goes well, you can run the screen script normally. 20 | -------------------------------------------------------------------------------- /screens/agenda/agenda.kv: -------------------------------------------------------------------------------- 1 | 2 | calendar_grid: calendar_grid 3 | 4 | ScrollView: 5 | id: agenda_scroll 6 | size_hint: 1, 1 7 | 8 | GridLayout: 9 | id: calendar_grid 10 | size_hint: 1, None 11 | height: self.minimum_height 12 | cols: 1 13 | padding: 5 14 | spacing: 2 15 | 16 | 17 | size_hint: 1, None 18 | height: 25 19 | cols: 2 20 | 21 | canvas.before: 22 | Color: 23 | rgba: 0, 0, 0.5, 1 24 | Rectangle: 25 | size: self.size 26 | pos: self.pos 27 | 28 | Label: 29 | text: root.header 30 | size_hint_x: None 31 | text_size: self.size 32 | width: 250 33 | halign: "left" 34 | padding: 10, 5 35 | 36 | Label: 37 | text: "" 38 | 39 | 40 | 41 | cols: 2 42 | size_hint: 1, None 43 | height: self.minimum_height 44 | 45 | Widget: 46 | size_hint_x: 0.05 47 | 48 | canvas.before: 49 | Color: 50 | rgba: root.bgcolour 51 | Rectangle: 52 | size: self.size 53 | pos: self.pos 54 | 55 | GridLayout: 56 | rows: 2 57 | size_hint_y: None 58 | height: self.minimum_height 59 | 60 | Label: 61 | id: lbl_evtime 62 | color: 1, 1, 1, 1 63 | size_hint_y: None 64 | height: 25 65 | text: root.evdetail 66 | text_size: self.size 67 | halign: "left" 68 | padding: 15, 0 69 | 70 | Label: 71 | id: lbl_evdetail 72 | color: 0.5, 0.5, 0.5, 1 73 | size_hint_y: None 74 | height: 20 75 | text: root.evtime 76 | text_size: self.size 77 | halign: "left" 78 | padding: 15, 0 79 | -------------------------------------------------------------------------------- /screens/agenda/authorise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import httplib2 3 | import os 4 | import sys 5 | 6 | from apiclient import discovery 7 | import oauth2client 8 | from oauth2client import client 9 | from oauth2client import tools 10 | 11 | try: 12 | import argparse 13 | flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() 14 | except ImportError: 15 | flags = None 16 | 17 | CUR_DIR = os.path.dirname(os.path.abspath(__file__)) 18 | 19 | SCOPES = 'https://www.googleapis.com/auth/calendar.readonly' 20 | CLIENT_SECRET_FILE = 'client_secret.json' 21 | APPLICATION_NAME = 'RPi Information Screen' 22 | 23 | SECRET = os.path.join(CUR_DIR, "secret", CLIENT_SECRET_FILE) 24 | 25 | 26 | def get_credentials(): 27 | """Gets valid user credentials from storage. 28 | 29 | If nothing has been stored, or if the stored credentials are invalid, 30 | the OAuth2 flow is completed to obtain the new credentials. 31 | 32 | Returns: 33 | Credentials, the obtained credential. 34 | """ 35 | home_dir = os.path.expanduser('~') 36 | credential_dir = os.path.join(home_dir, '.credentials') 37 | if not os.path.exists(credential_dir): 38 | os.makedirs(credential_dir) 39 | credential_path = os.path.join(credential_dir, 40 | 'calendar-python-quickstart.json') 41 | 42 | store = oauth2client.file.Storage(credential_path) 43 | credentials = store.get() 44 | if not credentials or credentials.invalid: 45 | flow = client.flow_from_clientsecrets(SECRET, SCOPES) 46 | flow.user_agent = APPLICATION_NAME 47 | if flags: 48 | credentials = tools.run_flow(flow, store, flags) 49 | else: # Needed only for compatability with Python 2.6 50 | credentials = tools.run(flow, store) 51 | print('Storing credentials to ' + credential_path) 52 | return credentials 53 | 54 | if __name__ == "__main__": 55 | get_credentials() 56 | -------------------------------------------------------------------------------- /screens/agenda/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "AgendaScreen", 3 | "kv": "agenda.kv", 4 | "dependencies": ["dateutil", "pytz"], 5 | "enabled": false, 6 | "params": { 7 | "max_days": 90 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /screens/clock/clock.kv: -------------------------------------------------------------------------------- 1 | : 2 | Label: 3 | pos: 0, 0 4 | font_name: "screens/clock/resources/SFDigitalReadout-Medium.ttf" 5 | text: "[size=200]{h:02d}:{m:02d}[/size][size=100] {s:02d}[/size]".format(**root.timedata) 6 | markup: True 7 | -------------------------------------------------------------------------------- /screens/clock/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "ClockScreen", 3 | "kv": "clock.kv", 4 | "enabled": false 5 | } 6 | -------------------------------------------------------------------------------- /screens/clock/resources/SFDigitalReadout-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/clock/resources/SFDigitalReadout-Medium.ttf -------------------------------------------------------------------------------- /screens/clock/screen.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from kivy.properties import DictProperty 4 | from kivy.clock import Clock 5 | from kivy.uix.screenmanager import Screen 6 | 7 | 8 | class ClockScreen(Screen): 9 | """Simple plugin screen to show digital clock of current time.""" 10 | # String Property to hold time 11 | timedata = DictProperty(None) 12 | 13 | def __init__(self, **kwargs): 14 | self.get_time() 15 | super(ClockScreen, self).__init__(**kwargs) 16 | self.timer = None 17 | 18 | def get_time(self): 19 | """Sets self.timedata to current time.""" 20 | n = datetime.now() 21 | self.timedata["h"] = n.hour 22 | self.timedata["m"] = n.minute 23 | self.timedata["s"] = n.second 24 | 25 | def update(self, dt): 26 | self.get_time() 27 | 28 | def on_enter(self): 29 | # We only need to update the clock every second. 30 | self.timer = Clock.schedule_interval(self.update, 1) 31 | 32 | def on_pre_enter(self): 33 | self.get_time() 34 | 35 | def on_pre_leave(self): 36 | # Save resource by unscheduling the updates. 37 | Clock.unschedule(self.timer) 38 | -------------------------------------------------------------------------------- /screens/energenie/README: -------------------------------------------------------------------------------- 1 | Energenie Control Screen 2 | 3 | Very simple screen to manage the control of four switches. 4 | 5 | To use this screen, the Raspberry Pi connected to the Energenie transmitter must be running pigpiod. If you don't know how to do this, there are plenty of instructions on the forum. 6 | 7 | To set up, just change the "host" value of the conf.json file to the address of your Pi and change the names of the switches. 8 | -------------------------------------------------------------------------------- /screens/energenie/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/energenie/__init__.py -------------------------------------------------------------------------------- /screens/energenie/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "EnergenieScreen", 3 | "kv": "energenie.kv", 4 | "dependencies": ["pigpio"], 5 | "enabled": false, 6 | "params": { 7 | "host": "192.168.0.1", 8 | "switchnames": { 9 | "1": "Hall lamp", 10 | "2": "Bedroom", 11 | "3": "Kitchen", 12 | "4": "Radio" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /screens/energenie/energenie.kv: -------------------------------------------------------------------------------- 1 | : 2 | orientation: "vertical" 3 | size: 400, 200 4 | size_hint: None, None 5 | 6 | Label: 7 | text: root.switch_name 8 | font_size: 20 9 | size_hint_y: 0.2 10 | 11 | BoxLayout: 12 | orientation: "horizontal" 13 | spacing: 10 14 | padding: 80, 20 15 | 16 | Button: 17 | text: "ON" 18 | on_press: root.switch_on() 19 | 20 | Button: 21 | text: "OFF" 22 | on_press: root.switch_off() 23 | 24 | : 25 | nrg_box: nrg_box 26 | nrg_stack: nrg_stack 27 | 28 | BoxLayout: 29 | id: nrg_box 30 | orientation: "vertical" 31 | 32 | Label: 33 | id: eclabel 34 | size_hint_y: None 35 | height: 50 36 | text: "Energenie Control Screen" 37 | font_size: 25 38 | 39 | StackLayout: 40 | id: nrg_stack 41 | orientation: "lr-tb" 42 | -------------------------------------------------------------------------------- /screens/energenie/energenie_pigpio.py: -------------------------------------------------------------------------------- 1 | # Module for Energenie switches using pigpio module 2 | # All credit due to the original energenie.py scrip by Amy Mather 3 | # See: https://github.com/MiniGirlGeek/energenie-demo/blob/master/energenie.py 4 | 5 | import pigpio 6 | from time import sleep 7 | 8 | # The GPIO pins for the Energenie module 9 | BIT1 = 17 # Board 11 10 | BIT2 = 22 # Board 15 11 | BIT3 = 16 # Board 16 12 | BIT4 = 21 # Board 13 13 | 14 | ON_OFF_KEY = 24 # Board 18 15 | ENABLE = 25 # Board 22 16 | 17 | # Codes for switching on and off the sockets 18 | # all 1 2 3 4 19 | ON = ['1011', '1111', '1110', '1101', '1100'] 20 | OFF = ['0011', '0111', '0110', '0101', '0100'] 21 | 22 | 23 | class EnergenieControl(object): 24 | def __init__(self, **kwargs): 25 | self.host = kwargs.get("host", "") 26 | self.connect() 27 | self.setup() 28 | 29 | def connect(self): 30 | """Create an instance of the pigpio pi.""" 31 | self.pi = pigpio.pi(self.host) 32 | 33 | def setup(self): 34 | """Clear the pin values before we use the transmitter.""" 35 | if self.connected: 36 | self.pi.write(ON_OFF_KEY, False) 37 | self.pi.write(ENABLE, False) 38 | self.pi.write(BIT1, False) 39 | self.pi.write(BIT2, False) 40 | self.pi.write(BIT3, False) 41 | self.pi.write(BIT4, False) 42 | 43 | def __change_plug_state(self, socket, on_or_off): 44 | """Method to set up the pins and fire the transmitter.""" 45 | state = on_or_off[socket][3] == '1' 46 | self.pi.write(BIT1, state) 47 | state = on_or_off[socket][2] == '1' 48 | self.pi.write(BIT2, state) 49 | state = on_or_off[socket][1] == '1' 50 | self.pi.write(BIT3, state) 51 | state = on_or_off[socket][0] == '1' 52 | self.pi.write(BIT4, state) 53 | sleep(0.1) 54 | self.pi.write(ENABLE, True) 55 | sleep(0.25) 56 | self.pi.write(ENABLE, False) 57 | 58 | def switch_on(self, socket): 59 | self.__change_plug_state(socket, ON) 60 | 61 | def switch_off(self, socket): 62 | self.__change_plug_state(socket, OFF) 63 | 64 | @property 65 | def connected(self): 66 | return self.pi.connected 67 | -------------------------------------------------------------------------------- /screens/energenie/screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from kivy.properties import StringProperty, ObjectProperty 5 | from kivy.uix.boxlayout import BoxLayout 6 | from kivy.uix.label import Label 7 | from kivy.uix.screenmanager import Screen 8 | from kivy.uix.stacklayout import StackLayout 9 | 10 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 11 | 12 | from energenie_pigpio import EnergenieControl 13 | 14 | 15 | class EnergenieButton(BoxLayout): 16 | """Class for displaying controls for a custom switch. 17 | 18 | Displays the name of the switch (if set) and two buttons to turn the 19 | switch on and off. 20 | """ 21 | switch_name = StringProperty("") 22 | 23 | def __init__(self, switch_id, switch_name=None, control=None, **kwargs): 24 | super(EnergenieButton, self).__init__(**kwargs) 25 | 26 | # The ID of the switch tells the transmitter which switch it needs to 27 | # control 28 | self.switch_id = switch_id 29 | 30 | # Check if there's a friendly name for the switch 31 | if switch_name: 32 | self.switch_name = switch_name 33 | 34 | # but provide a fallback if not 35 | else: 36 | self.switch_name = "Switch #{}".format(self.switch_id) 37 | 38 | # Get the instance of the control object 39 | self.control = control 40 | 41 | # Events to switch the switch on and off. 42 | def switch_on(self): 43 | self.control.switch_on(self.switch_id) 44 | 45 | def switch_off(self): 46 | self.control.switch_off(self.switch_id) 47 | 48 | 49 | class EnergenieScreen(Screen): 50 | nrg_box = ObjectProperty(None) 51 | nrg_stack = ObjectProperty(None) 52 | 53 | def __init__(self, **kwargs): 54 | super(EnergenieScreen, self).__init__(**kwargs) 55 | self.running = False 56 | 57 | # Get the user's preferences 58 | self.host = kwargs["params"]["host"] 59 | self.switchnames = kwargs["params"].get("switchnames", {}) 60 | 61 | def on_enter(self): 62 | # If we've not managed to set up the screen yet... 63 | if not self.running: 64 | 65 | # Create an instance of the controller 66 | self.ec = EnergenieControl(host=self.host) 67 | 68 | # If we can't connect 69 | if not self.ec.connected: 70 | 71 | # Let the user know 72 | self.nrg_stack.clear_widgets() 73 | msg = "Cannot connect to server on {}".format(self.host) 74 | lbl = Label(text=msg) 75 | self.nrg_stack.add_widget(lbl) 76 | 77 | # if we can then we need to show the buttons 78 | else: 79 | self.nrg_stack.clear_widgets() 80 | 81 | # We're controlling switches 1 to 4 82 | for i in range(1, 5): 83 | 84 | # Create a button 85 | switch_name = self.switchnames.get(str(i), None) 86 | btn = EnergenieButton(i, 87 | switch_name=switch_name, 88 | control=self.ec) 89 | 90 | # Add to the display 91 | self.nrg_stack.add_widget(btn) 92 | 93 | # We're running 94 | self.running = True 95 | -------------------------------------------------------------------------------- /screens/finlandarrivals/README: -------------------------------------------------------------------------------- 1 | Finland Arrivals 2 | ------------------- 3 | 4 | Update the "params" section of the conf.json file to include the desired bus stops. 5 | 6 | Each entry in the "stops" section is a dictionary containing two entries: 7 | "description" - Your name for the bus stop 8 | "stopid" - the identifier for the bus stop 9 | 10 | You can find the identifer codes on the HSL website. 11 | 12 | Navigate to: 13 | 14 | http://dev.hsl.fi/graphql/console/?query={agency%28id%3A%22HSL%22%29%20{routes%20{stops%20{gtfsId%20name%20code}}}} 15 | 16 | There is propably a way in the near future to find this ID by searching for the "code" field. But this is not the case today. 17 | -------------------------------------------------------------------------------- /screens/finlandarrivals/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "FinlandArrivalsScreen", 3 | "kv": "finlandarrivals.kv", 4 | "enabled": false, 5 | "dependencies": ["requests"], 6 | "params": { 7 | "stops": [ 8 | {"description": "Pasila", 9 | "stopid": "HSL:1174552"}, 10 | {"description": "Merituuli Kauppakeskus", 11 | "stopid": "HSL:2432218"}, 12 | {"description": "Niittyrinne XXL", 13 | "stopid": "HSL:2432205"}, 14 | {"description": "Finnoonsolmu", 15 | "stopid": "HSL:2432201"}, 16 | {"description": "Hannuksentie", 17 | "stopid": "HSL:2442204"} 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /screens/finlandarrivals/finlandarrivals.kv: -------------------------------------------------------------------------------- 1 | 2 | FloatLayout: 3 | id: lbus_float 4 | 5 | BoxLayout: 6 | id: lbus_base_box 7 | Label: 8 | id: weather_lbl_load 9 | text: "Loading weather..." 10 | 11 | ScreenManager: 12 | id: lbus_scrmgr 13 | auto_bring_to_front: False 14 | 15 | HiddenButton: 16 | size: root.width, 50 17 | size_hint: None, None 18 | pos: 0, root.height - 50 19 | on_press: root.next_screen() 20 | 21 | HiddenButton: 22 | size: root.width, 50 23 | size_hint: None, None 24 | pos: 0, 0 25 | on_press: root.next_screen(False) 26 | 27 | 28 | BoxLayout: 29 | orientation: "vertical" 30 | spacing: 10 31 | 32 | BoxLayout: 33 | size_hint_y: 0.2 34 | orientation: "horizontal" 35 | padding: 50,0 36 | id: platform 37 | 38 | Label: 39 | text_size: self.size 40 | halign: 'left' 41 | valign: 'middle' 42 | text: root.description 43 | font_size: 30 44 | 45 | Label: 46 | text_size: self.size 47 | halign: 'right' 48 | valign: 'middle' 49 | font_name: "screens/finlandarrivals/resources/SFDigitalReadout-Medium.ttf" 50 | text: root.timedata 51 | font_size: 60 52 | 53 | # BoxLayout: 54 | # orientation: "vertical" 55 | # size_hint_y: 0.2 56 | # 57 | # Label: 58 | # text: "Filter" 59 | # halign: "left" 60 | # size_hint_y: 0.4 61 | 62 | BoxLayout: 63 | orientation: "horizontal" 64 | id: bx_filter 65 | padding: 50,0 66 | size_hint_y: 0.15 67 | 68 | BoxLayout: 69 | size_hint_y: 0.6 70 | id: bx_buses 71 | orientation: "vertical" 72 | 73 | BoxLayout: 74 | orientation: "horizontal" 75 | height: 40 76 | size_hint_y: 0.05 77 | 78 | Label: 79 | text_size: self.size 80 | padding_x: 5 81 | halign: "left" 82 | font_size: 10 83 | text: "Data provided by Digitransit" 84 | size_hint_x: 0.2 85 | 86 | Label: 87 | padding_x: 5 88 | text_size: self.size 89 | halign: "right" 90 | valign: "middle" 91 | font_size: 20 92 | text: root.alert 93 | size_hint_x: 0.8 94 | 95 | 96 | 97 | orientation: "horizontal" 98 | height: 30 99 | size_hint_y: None 100 | font_name: "NotoSansMono.ttf" 101 | 102 | Label: 103 | text_size: self.size 104 | halign: 'right' 105 | valign: 'middle' 106 | size_hint_x: 0.1 107 | font_name: "NotoSans-Bold.ttf" 108 | text: root.bus_time 109 | color: 1,1,0,1 # Yellow 110 | Label: 111 | size_hint_x: 0.1 112 | text: root.bus_route 113 | Label: 114 | text_size: self.size 115 | halign: 'left' 116 | valign: 'middle' 117 | size_hint_x: 0.6 118 | text: root.bus_destination 119 | Label: 120 | size_hint_x: 0.2 121 | text: root.bus_delay 122 | color: 1,0,0,1 # Red 123 | 124 | -------------------------------------------------------------------------------- /screens/finlandarrivals/finlandarrivals.py: -------------------------------------------------------------------------------- 1 | """This script demonstrates how to retrieve bus countdown information 2 | from the digitransit.fi website and turn it into a structure that can then 3 | be used by other python codes. 4 | """ 5 | 6 | # Simple way of submitting web requests (easier to use than urllib2) 7 | import requests 8 | 9 | # We need json to turn the JSON response into a python dict/list 10 | import json 11 | 12 | # We need datetime to calculate the amount of time until the bus is departure 13 | from datetime import datetime 14 | 15 | # We need time to get the local time 16 | import time 17 | 18 | # This is the address used for the bus stop information. 19 | # We'll need the bus stop ID but we'll set this when calling 20 | # out lookup function so, for now, we leave a placeholder for it. 21 | BASE_URL = ("http://api.digitransit.fi/routing/v1/routers/finland/index/graphql") 22 | data = \ 23 | "{" \ 24 | " stop(id: \"%s\") {" \ 25 | " stoptimesWithoutPatterns(numberOfDepartures:20) {" \ 26 | " trip{" \ 27 | " route{" \ 28 | " shortName longName type" \ 29 | " }" \ 30 | " alerts{" \ 31 | " alertDescriptionTextTranslations {" \ 32 | " text" \ 33 | " language" \ 34 | " }" \ 35 | " }" \ 36 | " }" \ 37 | " scheduledDeparture departureDelay serviceDay" \ 38 | " }" \ 39 | " }" \ 40 | "}" 41 | 42 | def __getBusData(stopcode): 43 | # Add the stop code to the web address and get the page 44 | r = requests.post(BASE_URL, data=data%stopcode, headers={"Content-type": "application/graphql"}) 45 | 46 | # If the request was ok 47 | if r.status_code == 200: 48 | 49 | # try and load the response into JSON 50 | j = json.loads(r.content) 51 | return j['data']['stop']['stoptimesWithoutPatterns'] 52 | 53 | else: 54 | return None 55 | 56 | def datetime_from_utc_to_local(utc_datetime): 57 | now_timestamp = time.time() 58 | offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(now_timestamp) 59 | return utc_datetime + offset 60 | 61 | def __getBusTime(epoch, departuretime, delaytime): 62 | """Function to convert the arrival time into something a bit more 63 | meaningful. 64 | 65 | Takes a UTC epoch time argument and returns the number of minutes until 66 | that time. 67 | """ 68 | # Convert the epoch time number into a datetime object 69 | bustime = datetime.utcfromtimestamp(epoch+departuretime) 70 | realbustime = datetime.utcfromtimestamp(epoch+departuretime+delaytime) 71 | localtime = datetime_from_utc_to_local(bustime) 72 | estimatedtime = datetime_from_utc_to_local(realbustime) 73 | # Calculate the difference between now and the arrival time 74 | # The difference is a timedelta object. 75 | diff = bustime - datetime.utcnow() 76 | # return both the formatted string and delay 77 | return "{:%H:%M}".format(localtime), diff, "{:%H:%M}".format(estimatedtime) 78 | 79 | 80 | def BusLookup(stopcode): 81 | """Method to look up bus arrival times at a given bus stop. 82 | 83 | Takes two parameters: 84 | 85 | stopcode: ID code of desired stop 86 | 87 | Returns a list of dictionaries representing a bus: 88 | 89 | route: String representing the bus route number 90 | time: String representing the due time of the bus 91 | delta: Timedelta object representing the time until arrival 92 | 93 | The list is sorted in order of arrival time with the nearest bus first. 94 | """ 95 | 96 | buslist = __getBusData(stopcode) 97 | 98 | buses = [] 99 | 100 | # Loop through the buses in our response 101 | for bus in buslist: 102 | # Create an empty dictionary for the details 103 | b = {} 104 | # Set the route number of the bus 105 | b["route"] = bus['trip']['route']['shortName'] 106 | if not b["route"]: 107 | b["route"] = u"0" 108 | # Set the transport type 109 | b["type"] = str(bus['trip']['route']['type']) 110 | # Set the destination of the bus 111 | b["destination"] = bus['trip']['route']['longName'] 112 | # Get the string time and timedelta object of the bus 113 | b["time"], b["delta"], b["estimated"] = __getBusTime(bus['serviceDay'], bus['scheduledDeparture'], bus['departureDelay']) 114 | # Unpack this into minutes and seconds (but we will discard the seconds) 115 | minutes, _ = divmod(b["delta"].total_seconds(), 60) 116 | delay = bus['departureDelay'] 117 | if delay <= -60: 118 | b["delay"] = "Running ahead {m:.0f} minutes".format(m=minutes) 119 | elif delay < 180: 120 | b["delay"] = "" 121 | else: 122 | b["delay"] = "Estimated "+b["estimated"] 123 | alerts = bus['trip']['alerts'] 124 | for alert in alerts: 125 | if "alert" in(b): 126 | b["alert"] += bus['trip']['alerts']['alertDescriptionTextTranslations']['text'] 127 | else: 128 | b["alert"] = bus['trip']['alerts']['alertDescriptionTextTranslations']['text'] 129 | # Add the bus to our list 130 | buses.append(b) 131 | 132 | # We sort the buses in order of their arrival time (the raw response is 133 | # not always provided in time order) 134 | # To do this we sort by the timedelta object as this is the most accurate 135 | # information we have on the buses 136 | return sorted(buses, key=lambda x: x["delta"]) 137 | -------------------------------------------------------------------------------- /screens/finlandarrivals/resources/SFDigitalReadout-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/finlandarrivals/resources/SFDigitalReadout-Medium.ttf -------------------------------------------------------------------------------- /screens/football/README: -------------------------------------------------------------------------------- 1 | Football Scores plugin 2 | ---------------------- 3 | 4 | This screen can display updates for individual teams or for leagues (including cup competitions). There is no limit on the number of teams/leagues you wish to include. 5 | 6 | Details of the available teams and league ids can be found in the appropriately titled files in this folder. 7 | 8 | You should amend the "params" section of the conf.json as required. 9 | 10 | For example: 11 | 12 | "params": { 13 | "teams": ["Man City", "Chelsea"], 14 | "leagues": ["118998037"] 15 | } 16 | 17 | Would provide team updates for Manchester City and Chelsea and show all League Cup scores. 18 | 19 | If you don't want updates for either teams or leagues, then the value should be set as an empty list e.g. 20 | 21 | "teams": [], 22 | "leagues": ["118998037"] 23 | -------------------------------------------------------------------------------- /screens/football/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/football/__init__.py -------------------------------------------------------------------------------- /screens/football/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "FootballScreen", 3 | "kv": "football.kv", 4 | "enabled": false, 5 | "dependencies": ["requests", "BeautifulSoup"], 6 | "params": { 7 | "teams": ["Chelsea"], 8 | "leagues": ["119001074"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /screens/football/football.kv: -------------------------------------------------------------------------------- 1 | 2 | id: football_lbl_goal 3 | text: root.event_text 4 | font_size: 0 5 | bgcolour: 0,0,0,0 6 | 7 | 8 | FloatLayout: 9 | id: base_float 10 | Label: 11 | id: lbl_load 12 | text: "Searching for {} matches".format(root.teamname) 13 | 14 | 15 | FloatLayout: 16 | id: football_float 17 | 18 | BoxLayout: 19 | id: football_base_box 20 | Label: 21 | id: football_lbl_load 22 | text: "Searching for match..." 23 | 24 | ScreenManager: 25 | id: football_scrmgr 26 | auto_bring_to_front: False 27 | 28 | HiddenButton: 29 | size: root.width, 50 30 | size_hint: None, None 31 | pos: 0, root.height - 50 32 | on_press: root.next_screen() 33 | 34 | HiddenButton: 35 | size: root.width, 50 36 | size_hint: None, None 37 | pos: 0, 0 38 | on_press: root.next_screen(False) 39 | 40 | 41 | Label: 42 | text: "{} are not playing today.".format(root.teamname) 43 | 44 | 45 | BoxLayout: 46 | orientation: "vertical" 47 | 48 | BoxLayout: 49 | orientation: "horizontal" 50 | size_hint_y: 0.2 51 | 52 | AsyncImage: 53 | source: root.homebadge 54 | 55 | AsyncImage: 56 | source: root.awaybadge 57 | 58 | BoxLayout: 59 | orientation: "horizontal" 60 | size_hint_y: 0.3 61 | 62 | Label: 63 | text: root.hometeam 64 | font_size: 40 65 | 66 | Label: 67 | text: root.homescore 68 | font_size: 45 69 | size_hint_x: 0.15 70 | 71 | Label: 72 | text: "-" 73 | size_hint_x: 0.1 74 | 75 | Label: 76 | text: root.awayscore 77 | font_size: 45 78 | size_hint_x: 0.15 79 | 80 | Label: 81 | text: root.awayteam 82 | font_size: 40 83 | 84 | BoxLayout: 85 | orientation: "horizontal" 86 | size_hint_y: 0.1 87 | 88 | Label: 89 | text: root.status 90 | 91 | BoxLayout: 92 | orientation: "horizontal" 93 | size_hint_y: 0.4 94 | valign: "top" 95 | 96 | StackLayout: 97 | id: home_incidents 98 | width: 350 99 | size_hint: None, 1 100 | orientation: "tb-lr" 101 | 102 | Label: 103 | width: 100 104 | size_hint_y: None 105 | text: "" 106 | 107 | StackLayout: 108 | id: away_incidents 109 | width: 350 110 | size_hint: None, 1 111 | orientation: "tb-lr" 112 | 113 | 114 | FloatLayout: 115 | id: base_float 116 | 117 | BoxLayout: 118 | id: league_box 119 | orientation: "vertical" 120 | 121 | Label: 122 | text: root.leaguename 123 | size_hint_y: 0.2 124 | 125 | 126 | orientation: "vertical" 127 | height: 40 128 | 129 | BoxLayout: 130 | orientation: "horizontal" 131 | size_hint_y: 0.7 132 | spacing: 5 133 | 134 | BGLabel: 135 | text: root.hometeam 136 | halign: "right" 137 | 138 | BGLabel: 139 | text: root.homescore 140 | bgcolour: root.homebg 141 | size_hint_x: None 142 | width: 25 143 | 144 | BGLabel: 145 | text: root.awayscore 146 | bgcolour: root.awaybg 147 | size_hint_x: None 148 | width: 25 149 | 150 | BGLabel: 151 | text: root.awayteam 152 | halign: "left" 153 | 154 | Label: 155 | text: root.status 156 | font_size: 10 157 | valign: "top" 158 | 159 | 160 | orientation: "vertical" 161 | size: 750, 200 162 | pos: 25, 140 163 | size_hint: None, None 164 | canvas.before: 165 | Color: 166 | rgba: 0.1,0.1,0.1,1 167 | Rectangle: 168 | pos: self.pos 169 | size: self.size 170 | on_press: root.parent.remove_widget(root) 171 | 172 | BoxLayout: 173 | orientation: "horizontal" 174 | size_hint_y: 0.4 175 | spacing: 5 176 | padding: 10, 10, 10, 10 177 | 178 | BGLabel: 179 | text: root.hometeam 180 | halign: "right" 181 | font_size: 40 182 | 183 | BGLabel: 184 | text: root.homescore 185 | bgcolour: root.homebg 186 | size_hint_x: None 187 | width: 40 188 | font_size: 40 189 | 190 | BGLabel: 191 | text: root.awayscore 192 | bgcolour: root.awaybg 193 | size_hint_x: None 194 | width: 40 195 | font_size: 40 196 | 197 | BGLabel: 198 | text: root.awayteam 199 | halign: "left" 200 | font_size: 40 201 | 202 | Label: 203 | text: root.matchtime 204 | font_size: 15 205 | valign: "top" 206 | size_hint_y: 0.1 207 | 208 | BoxLayout: 209 | orientation: "horizontal" 210 | size_hint_y: 0.5 211 | spacing: 5 212 | valign: "top" 213 | 214 | BGLabel: 215 | text: root.homescorers 216 | halign: "right" 217 | 218 | BGLabel: 219 | size_hint_x: None 220 | width: 60 221 | 222 | BGLabel: 223 | size_hint_x: None 224 | width: 60 225 | 226 | BGLabel: 227 | text: root.awayscorers 228 | halign: "left" 229 | 230 | 231 | orientation: "horizontal" 232 | size_hint: None, None 233 | size: 350, 20 234 | 235 | Label: 236 | #text: "A" if not root.home else "" 237 | size_hint_x: 0.1 238 | 239 | Label: 240 | text: root.player 241 | halign: "right" if root.home else "left" 242 | 243 | Label: 244 | #text: "H" if root.home else "" 245 | size_hint_x: 0.1 246 | 247 | 248 | text: str(root.height) 249 | 250 | 251 | Label: 252 | text: root.errormessage 253 | 254 | # 255 | # height: 42 256 | # orientation: "vertical" 257 | # 258 | # BoxLayout: 259 | # orientation: "horizontal" 260 | # size_hint_y: 0.7 261 | # 262 | # Label: 263 | # text: "<-" 264 | # 265 | # Label: 266 | # text: root.lb 267 | # size_hint_x: 0.9 268 | # 269 | # Label: 270 | # text: "->" 271 | # 272 | # Label: 273 | # text: "-----------" 274 | # 275 | # 276 | # size_hint: None, None 277 | # size: 400, 200 278 | # pos: 200, 140 279 | # 280 | # text: "This should be on top" 281 | # font_size: 40 282 | # 283 | # canvas.before: 284 | # Color: 285 | # rgba: 1,0,0,1 286 | # Rectangle: 287 | # size: self.size 288 | # pos: self.pos 289 | -------------------------------------------------------------------------------- /screens/football/footballresources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/football/footballresources/__init__.py -------------------------------------------------------------------------------- /screens/football/leagues.txt: -------------------------------------------------------------------------------- 1 | 118996114 Premier League 2 | 118996115 Championship 3 | 118996116 League One 4 | 118996117 League Two 5 | 118996118 Football Conference 6 | 118996118 National League 7 | 118996176 Scottish Premiership 8 | 118996177 Scottish Championship 9 | 118996178 Scottish League One 10 | 118996179 Scottish League Two 11 | 118996207 Welsh Premier League 12 | 118996238 Irish Premiership 13 | 118996240 League of Ireland Premier 14 | 118996307 Conference North 15 | 118996307 National League North 16 | 118996308 Conference South 17 | 118996308 National League South 18 | 118998036 FA Cup 19 | 118998037 League Cup 20 | 118998038 Football League Trophy 21 | 118998039 FA Cup Qualifying 22 | 118998041 The FA Trophy 23 | 118998043 Irish Cup 24 | 118998098 Scottish Cup 25 | 118998099 Scottish League Cup 26 | 118998100 Scottish Challenge Cup 27 | 118998129 Welsh Cup 28 | 118998132 FA Women's Cup 29 | 118999031 Highland League 30 | 118999958 Champions League 31 | 118999989 Europa League 32 | 119000051 Super Cup 33 | 119000919 Austrian Bundesliga 34 | 119000924 Belgian Pro League 35 | 119000950 Danish Superliga 36 | 119000955 Finnish Veikkausliiga 37 | 119000981 French Ligue 1 38 | 119000986 German Bundesliga 39 | 119001012 Dutch Eredivisie 40 | 119001017 Italian Serie A 41 | 119001043 Norwegian Tippeligaen 42 | 119001048 Portuguese Primeira Liga 43 | 119001074 Spanish La Liga 44 | 119001079 Swedish Allsvenskan 45 | 119001105 Swiss Super League 46 | 119001110 Turkish Super Lig 47 | 119001136 Greek Superleague 48 | 119001914 Africa Cup of Nations 49 | 119001943 International Friendlies 50 | 119001974 Under-21 Friendly 51 | 119002004 Euro Under-21 Qualifying 52 | 119002005 Euro Under-21 Championship 53 | 119002036 European Championship Qualifying 54 | 119003058 Women's World Cup 55 | 119003064 Club World Cup 56 | 119003989 Women's Super League 57 | 119003997 Lowland League 58 | 119004019 Women's Champions League 59 | 999999983 Gold Cup 60 | 999999984 Copa America 61 | 999999987 Women's International Friendlies 62 | 999999988 United States Major League Soccer 63 | 999999989 Spanish Copa del Rey 64 | 999999990 Russian Premier League 65 | 999999991 Women's Premier North 66 | 999999992 Women's Premier South 67 | 999999993 Women's Super League 2 68 | 999999994 Argentine Primera Division 69 | 999999995 Australian A-League 70 | 999999996 Brazilian Campeonato Série A 71 | 999999997 French Coupe de France 72 | 999999998 German DFB Cup 73 | 999999999 Italian Coppa Italia 74 | -------------------------------------------------------------------------------- /screens/football/web.py: -------------------------------------------------------------------------------- 1 | import json 2 | import bottle 3 | import os 4 | import requests 5 | 6 | plugin_path = os.path.dirname(__file__) 7 | plugin = os.path.basename(plugin_path) 8 | all_teams = os.path.join(plugin_path, "teams.txt") 9 | all_leagues = os.path.join(plugin_path, "leagues.txt") 10 | TEAM_OPTION = """\n""" 11 | LEAGUE_OPTION = """\n""" 12 | 13 | LAYOUT = """% rebase("base.tpl", title="Configure Football Scores Screen") 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Select Teams{teamselect}
Select Leagues{leagueselect}
26 |
27 | """ 28 | 29 | bindings = [("/footballscores", "show_teams", ["GET"]), 30 | ("/footballscores/update", "update", ["POST"])] 31 | 32 | def show_teams(): 33 | host = bottle.request.get_header('host') 34 | addr = "http://localhost:8089/api/{plugin}/configure".format(host=host, 35 | plugin=plugin) 36 | getconfig = requests.get(addr).json() 37 | 38 | with open(all_teams, "r") as teamfile: 39 | teams = [x.strip() for x in teamfile.readlines()] 40 | 41 | ts = ("""" 48 | 49 | with open(all_leagues, "r") as leaguefile: 50 | leagues = [x.strip().split("\t") for x in leaguefile.readlines()] 51 | 52 | ls = """""" 61 | 62 | tpl = LAYOUT.format(teamselect=ts, leagueselect=ls) 63 | 64 | return bottle.template(tpl) 65 | 66 | def update(): 67 | host = bottle.request.get_header('host') 68 | addr = "http://localhost:8089/api/{plugin}/configure".format(host=host, 69 | plugin=plugin) 70 | leagues = bottle.request.forms.getall("leagues") 71 | teams = bottle.request.forms.getall("teams") 72 | data = {"teams": teams, "leagues": leagues} 73 | print data 74 | headers = {"Content-Type": "application/json; charset=utf8"} 75 | r = requests.post(addr, headers=headers, data=json.dumps(data)) 76 | j = r.json() 77 | if j["status"] == "success": 78 | bottle.redirect("/") 79 | else: 80 | return "Error." 81 | -------------------------------------------------------------------------------- /screens/isstracker/README: -------------------------------------------------------------------------------- 1 | ISS Tracker 2 | ----------- 3 | 4 | Screen to show current position of ISS. 5 | 6 | There are no user settings required. 7 | 8 | The MapView dependency needs Kivy garden: 9 | sudo apt-get install kivy-garden 10 | garden install mapview 11 | -------------------------------------------------------------------------------- /screens/isstracker/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "ISSScreen", 3 | "dependencies": [ 4 | "ephem", 5 | "concurrent", 6 | "requests" 7 | ], 8 | "enabled": false, 9 | "kv": "isstracker.kv" 10 | } 11 | -------------------------------------------------------------------------------- /screens/isstracker/images/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/isstracker/images/dot.png -------------------------------------------------------------------------------- /screens/isstracker/iss_tle.json: -------------------------------------------------------------------------------- 1 | { 2 | "updated": 1481360279.026718, 3 | "tle": [ 4 | "ISS (ZARYA)", 5 | "1 25544U 98067A 16345.25971633 .00005580 00000-0 92357-4 0 9993", 6 | "2 25544 51.6439 260.0841 0005883 314.4887 146.0880 15.53854424 32373" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /screens/isstracker/isstracker.kv: -------------------------------------------------------------------------------- 1 | : 2 | 3 | -------------------------------------------------------------------------------- /screens/isstracker/screen.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import os 3 | import json 4 | 5 | from kivy.uix.label import Label 6 | from kivy.uix.screenmanager import Screen 7 | from kivy.garden.mapview import MapView, MapMarker, MarkerMapLayer 8 | from kivy.clock import Clock 9 | 10 | import requests 11 | import ephem 12 | 13 | class ISSScreen(Screen): 14 | def __init__(self, **kwargs): 15 | super(ISSScreen, self).__init__(**kwargs) 16 | 17 | # Set the path for the folder 18 | self.path = os.path.dirname(os.path.abspath(__file__)) 19 | 20 | # Set the path for local images 21 | self.imagefolder = os.path.join(self.path, "images") 22 | 23 | # Ephem calculates the position using the Two Line Element data 24 | # We need to make sure we have up to date info 25 | tle = self.get_TLE() 26 | 27 | # Create an iss object from which we can get positional data 28 | self.iss = ephem.readtle(*tle) 29 | 30 | # Run the calculations 31 | self.iss.compute() 32 | 33 | # Get positon of iss and place a marker there 34 | lat, lon = self.get_loc() 35 | self.marker = MapMarker(lat=lat, lon=lon) 36 | 37 | # Create a value to check when we last drew ISS path 38 | self.last_path_update = 0 39 | 40 | # Create the path icon 41 | self.path_icon = os.path.join(self.imagefolder, "dot.png") 42 | 43 | # Create the world map 44 | self.map = MapView(id="mpv",lat=0, lon=0, zoom=1, scale=1.5) 45 | x, y = self.map.get_window_xy_from(0,0,1) 46 | self.map.scale_at(1.2, x, y) 47 | 48 | # Add the ISS marker to the map and draw the map on the screen 49 | self.map.add_widget(self.marker) 50 | self.add_widget(self.map) 51 | 52 | # Add a new layer for the path 53 | self.mmlayer = MarkerMapLayer() 54 | 55 | self.draw_iss_path() 56 | 57 | self.timer = None 58 | 59 | def on_enter(self): 60 | 61 | self.timer = Clock.schedule_interval(self.update, 1) 62 | 63 | def on_leave(self): 64 | 65 | Clock.unschedule(self.timer) 66 | 67 | def utcnow(self): 68 | return (datetime.utcnow() - datetime(1970,1,1)).total_seconds() 69 | 70 | def draw_iss_path(self): 71 | 72 | # Path is drawn every 5 mins 73 | if self.utcnow() - self.last_path_update > 30: 74 | 75 | try: 76 | self.map.remove_layer(self.mmlayer) 77 | except: 78 | pass 79 | 80 | self.mmlayer = MarkerMapLayer() 81 | 82 | # Create markers showing the ISS's position every 5 mins 83 | for i in range(20): 84 | lat, lon = self.get_loc(datetime.now() + timedelta(0, i * 300)) 85 | self.mmlayer.add_widget(MapMarker(lat=lat, 86 | lon=lon, 87 | source=self.path_icon)) 88 | 89 | # Update the flag so we know when next update should be run 90 | self.last_path_update = self.utcnow() 91 | 92 | # Add the layer and call the reposition function otherwise the 93 | # markers don't show otherwise! 94 | self.map.add_layer(self.mmlayer) 95 | self.mmlayer.reposition() 96 | 97 | def get_TLE(self): 98 | 99 | # Set some flags 100 | need_update = False 101 | 102 | # Set our data source and the name of the object we're tracking 103 | source = "http://www.celestrak.com/NORAD/elements/stations.txt" 104 | ISS = "ISS (ZARYA)" 105 | 106 | # Get the current time 107 | utc_now = self.utcnow() 108 | 109 | # Set the name of our file to store data 110 | data = os.path.join(self.path, "iss_tle.json") 111 | 112 | # Try loading old data 113 | try: 114 | with open(data, "r") as savefile: 115 | saved = json.load(savefile) 116 | 117 | # If we can't create a dummy dict 118 | except IOError: 119 | saved = {"updated": 0} 120 | 121 | # If old data is more than an hour hold, let's check for an update 122 | if utc_now - saved["updated"] > 3600: 123 | need_update = True 124 | 125 | # If we don't have any TLE data then we need an update 126 | if not saved.get("tle"): 127 | need_update = True 128 | 129 | if need_update: 130 | 131 | # Load the TLE data 132 | raw = requests.get(source).text 133 | 134 | # Split the data into a neat list 135 | all_sats = [sat.strip() for sat in raw.split("\n")] 136 | 137 | # Find the ISS and grab the whole TLE (three lines) 138 | iss_index = all_sats.index(ISS) 139 | iss_tle = all_sats[iss_index:iss_index + 3] 140 | 141 | # Prepare a dict to save our data 142 | new_tle = {"updated": utc_now, 143 | "tle": iss_tle} 144 | 145 | # Save it 146 | with open(data, "w") as savefile: 147 | json.dump(new_tle, savefile, indent=4) 148 | 149 | # ephem needs strings not unicode 150 | return [str(x) for x in iss_tle] 151 | 152 | else: 153 | # return the saved data (as strings) 154 | return [str(x) for x in saved["tle"]] 155 | 156 | def update(self, *args): 157 | 158 | # Update the ISS with newest TLE 159 | self.iss = ephem.readtle(*self.get_TLE()) 160 | 161 | # Get the position and update marker 162 | lat, lon = self.get_loc() 163 | self.marker.lat = lat 164 | self.marker.lon = lon 165 | self.map.remove_widget(self.marker) 166 | self.map.add_widget(self.marker) 167 | 168 | # Check if the path needs redrawing 169 | self.draw_iss_path() 170 | 171 | def get_loc(self, dt=None): 172 | 173 | # We can get the location for a specific time as well 174 | if dt is None: 175 | self.iss.compute() 176 | else: 177 | self.iss.compute(dt) 178 | 179 | # Convert the ephem data into something that the map can use 180 | lat = float(self.iss.sublat / ephem.degree) 181 | lon = float(self.iss.sublong / ephem.degree) 182 | 183 | return lat, lon 184 | -------------------------------------------------------------------------------- /screens/londonbus/README: -------------------------------------------------------------------------------- 1 | London Bus Arrivals 2 | ------------------- 3 | 4 | Update the "params" section of the conf.json file to include the desired bus stops. 5 | 6 | Each entry in the "stops" section is a dictionary containing two entries: 7 | "description" - Your name for the bus stop 8 | "stopid" - the 5-digit identifier for the bus stop 9 | 10 | You can find the identifer codes on the TfL website. 11 | 12 | Navigate to: 13 | 14 | http://countdown.tfl.gov.uk 15 | 16 | Search for the area you want, click on the bus stop and the 5 digit code will be shown beneath the map. 17 | -------------------------------------------------------------------------------- /screens/londonbus/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "LondonBusScreen", 3 | "kv": "londonbus.kv", 4 | "enabled": false, 5 | "dependencies": ["requests"], 6 | "params": { 7 | "stops": [ 8 | {"description": "Marble Arch", 9 | "stopid": 77064}, 10 | {"description": "Waterloo Road", 11 | "stopid": 76884} 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /screens/londonbus/londonbus.kv: -------------------------------------------------------------------------------- 1 | 2 | FloatLayout: 3 | id: lbus_float 4 | 5 | BoxLayout: 6 | id: lbus_base_box 7 | Label: 8 | id: weather_lbl_load 9 | text: "Loading weather..." 10 | 11 | ScreenManager: 12 | id: lbus_scrmgr 13 | auto_bring_to_front: False 14 | 15 | HiddenButton: 16 | size: root.width, 50 17 | size_hint: None, None 18 | pos: 0, root.height - 50 19 | on_press: root.next_screen() 20 | 21 | HiddenButton: 22 | size: root.width, 50 23 | size_hint: None, None 24 | pos: 0, 0 25 | on_press: root.next_screen(False) 26 | 27 | 28 | BoxLayout: 29 | orientation: "vertical" 30 | spacing: 10 31 | 32 | Label: 33 | size_hint_y: 0.2 34 | text: root.description 35 | 36 | # BoxLayout: 37 | # orientation: "vertical" 38 | # size_hint_y: 0.2 39 | # 40 | # Label: 41 | # text: "Filter" 42 | # halign: "left" 43 | # size_hint_y: 0.4 44 | 45 | BoxLayout: 46 | orientation: "horizontal" 47 | id: bx_filter 48 | padding: 50,0 49 | size_hint_y: 0.15 50 | 51 | BoxLayout: 52 | id: bx_buses 53 | orientation: "vertical" 54 | 55 | BoxLayout: 56 | orientation: "horizontal" 57 | height: 40 58 | size_hint_y: None 59 | 60 | Label: 61 | padding_x: 5 62 | halign: "left" 63 | font_size: 10 64 | text: "Data provided by TfL" 65 | size_hint_x: 0.5 66 | 67 | Label: 68 | padding_x: 5 69 | halign: "right" 70 | font_size: 10 71 | text: "" 72 | size_hint_x: 0.5 73 | 74 | 75 | 76 | orientation: "horizontal" 77 | height: 30 78 | size_hint_y: None 79 | 80 | Label: 81 | text: root.bus_route 82 | Label: 83 | text: root.bus_destination 84 | Label: 85 | text: root.bus_time 86 | -------------------------------------------------------------------------------- /screens/londonbus/londonbus.py: -------------------------------------------------------------------------------- 1 | """This script demonstrates how to retrieve bus countdown information 2 | from the TfL website and turn it into a structure that can then be used by 3 | other python codes. 4 | 5 | Unfortunately, the bus countdown information is not true JSON so, as you'll 6 | see from the code below, we need to play with it a bit first in order to 7 | parse it successfully. 8 | 9 | Please note, if you use TfL data in your application you should 10 | acknowledge the source of your data as appropriate. 11 | """ 12 | 13 | # Simple way of submitting web requests (easier to use than urllib2) 14 | import requests 15 | 16 | # We need json to turn the JSON response into a python dict/list 17 | import json 18 | 19 | # We need datetime to calculate the amount of time until the bus is departure 20 | from datetime import datetime 21 | 22 | # This is the address used for the bus stop information. 23 | # We'll need the bus stop ID (5 number code) but we'll set this when calling 24 | # out lookup function so, for now, we leave a placeholder for it. 25 | BASE_URL = ("http://countdown.api.tfl.gov.uk/interfaces/" 26 | "ura/instant_V1?StopCode1={stopcode}" 27 | "&ReturnList=LineName,DestinationText,EstimatedTime") 28 | 29 | 30 | def __getBusData(stopcode): 31 | # Add the stop code to the web address and get the page 32 | r = requests.get(BASE_URL.format(stopcode=stopcode)) 33 | 34 | # If the request was ok 35 | if r.status_code == 200: 36 | 37 | # try and load the response into JSON 38 | # However, the TFL data is not true JSON so we need a bit of 39 | # hackery first. The response is a string representing a list 40 | # on each line. 41 | # So first, we split the response into a list of lines 42 | rawdata = r.content.split("\r\n") 43 | # Then we turn this into a single line of list strings separated 44 | # by commas 45 | rawdata = ",".join(rawdata) 46 | # Lastly we wrap the string in list symbols so we have a string 47 | # representing a single list 48 | rawdata = "[{data}]".format(data=rawdata) 49 | # And now we can load into JSON to give us an actual list of lists 50 | return json.loads(rawdata) 51 | 52 | else: 53 | return None 54 | 55 | 56 | def __getBusTime(epoch): 57 | """Function to convert the arrival time into something a bit more 58 | meaningful. 59 | 60 | Takes a UTC epoch time argument and returns the number of minutes until 61 | that time. 62 | """ 63 | # The bus arrival time is in milliseconds but we want seconds. 64 | epoch = epoch / 1000 65 | # Convert the epoch time number into a datetime object 66 | bustime = datetime.utcfromtimestamp(epoch) 67 | # Calculate the difference between now and the arrival time 68 | # The difference is a timedelta object. 69 | diff = bustime - datetime.utcnow() 70 | # Unpack this into minutes and seconds (but we will discard the seconds) 71 | minutes, _ = divmod(diff.total_seconds(), 60) 72 | if minutes == 0: 73 | arrival = "Due" 74 | elif minutes == 1: 75 | arrival = "1 minute" 76 | else: 77 | arrival = "{m:.0f} minutes".format(m=minutes) 78 | 79 | # return both the formatted string and the timedelta object 80 | return arrival, diff 81 | 82 | 83 | def BusLookup(stopcode, filterbuses=None): 84 | """Method to look up bus arrival times at a given bus stop. 85 | 86 | Takes two parameters: 87 | 88 | stopcode: 5 digit ID code of desired stop 89 | filterbuses: list of bus routes to filter by. If omitted, all bus routes 90 | at the stop will be shown. 91 | 92 | If filterbuses receives anything other than a list then a TypeError will 93 | be raised. 94 | 95 | Returns a list of dictionaries representing a bus: 96 | 97 | route: String representing the bus route number 98 | time: String representing the due time of the bus 99 | delta: Timedelta object representing the time until arrival 100 | 101 | The list is sorted in order of arrival time with the nearest bus first. 102 | """ 103 | # filterbuses should be a list, if it's not then we need to alert the 104 | # user. 105 | if filterbuses is not None and type(filterbuses) != list: 106 | raise TypeError("filterbuses parameter must be a list.") 107 | 108 | buslist = __getBusData(stopcode) 109 | 110 | # Remove the first item from the list as it doesn't represent a bus 111 | buslist.pop(0) 112 | 113 | buses = [] 114 | 115 | # Loop through the buses in our response 116 | for bus in buslist: 117 | # Create an empty dictionary for the details 118 | b = {} 119 | # Set the route number of the bus 120 | b["route"] = bus[1] 121 | # Set the destination of the bus 122 | b["destination"] = bus[2] 123 | # Get the string time and timedelta object of the bus 124 | b["time"], b["delta"] = __getBusTime(bus[3]) 125 | # Add the bus to our list 126 | buses.append(b) 127 | 128 | # We sort the buses in order of their arrival time (the raw response is 129 | # not always provided in time order) 130 | # To do this we sort by the timedelta object as this is the most accurate 131 | # information we have on the buses 132 | buses = sorted(buses, key=lambda x: x["delta"]) 133 | 134 | # If the user has provided a list of buses then we filter our list so that 135 | # our result only includes the desired routes. 136 | if filterbuses: 137 | # Let's be nice to users, if they provide a list of integers (not 138 | # unreasonable for a bus route number) then we'll convert it into a 139 | # string to match what's stored in the dictionary 140 | # NB string is more appropriate here is as the number represents the 141 | # name of the route. 142 | filterbuses = [str(x) for x in filterbuses] 143 | # Just include buses that our in our list of requested routes 144 | buses = [x for x in buses if x["route"] in filterbuses] 145 | 146 | return buses 147 | -------------------------------------------------------------------------------- /screens/londonbus/screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import re 4 | 5 | from kivy.clock import Clock 6 | from kivy.uix.label import Label 7 | from kivy.uix.togglebutton import ToggleButton 8 | from kivy.uix.screenmanager import Screen 9 | from kivy.uix.boxlayout import BoxLayout 10 | from kivy.uix.stacklayout import StackLayout 11 | from kivy.uix.scrollview import ScrollView 12 | from kivy.properties import StringProperty, DictProperty 13 | 14 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 15 | 16 | import londonbus as LB 17 | 18 | # regex for natural sort. 19 | nsre = re.compile('([0-9]+)') 20 | 21 | 22 | # Define a natural sort method (thanks StackOverflow!) 23 | def natural_sort_key(s, _nsre=nsre): 24 | return [int(text) if text.isdigit() else text.lower() 25 | for text in re.split(_nsre, s)] 26 | 27 | 28 | class LondonBus(BoxLayout): 29 | """Custom widget to display bus information. 30 | 31 | Displays route name, destination and expected arrival time. 32 | """ 33 | bus_route = StringProperty("Loading...") 34 | bus_destination = StringProperty("Loading...") 35 | bus_time = StringProperty("Loading...") 36 | 37 | def __init__(self, **kwargs): 38 | super(LondonBus, self).__init__(**kwargs) 39 | bus = kwargs["bus"] 40 | self.bus_route = bus["route"] 41 | self.bus_destination = bus["destination"] 42 | self.bus_time = bus["time"] 43 | 44 | 45 | class LondonBusStop(Screen): 46 | """Custom screen class for showing countdown information for a specific 47 | bus stop. 48 | """ 49 | description = StringProperty("") 50 | 51 | def __init__(self, **kwargs): 52 | super(LondonBusStop, self).__init__(**kwargs) 53 | self.stop = kwargs["stop"] 54 | self.description = self.stop["description"] 55 | self.filters = None 56 | 57 | def on_enter(self): 58 | # Refresh the information when we load the screen 59 | Clock.schedule_once(self.get_buses, 0.5) 60 | 61 | # and schedule updates every 30 seconds. 62 | self.timer = Clock.schedule_interval(self.get_buses, 30) 63 | 64 | def on_leave(self): 65 | # Save resource by removing schedule 66 | Clock.unschedule(self.timer) 67 | 68 | def get_buses(self, *args): 69 | """Starts the process of retrieving countdown information.""" 70 | try: 71 | # Load the bus data. 72 | self.buses = LB.BusLookup(self.stop["stopid"]) 73 | except: 74 | # If there's an error (e.g. no internet connection) then we have 75 | # no bus data. 76 | self.buses = None 77 | 78 | if self.buses: 79 | # We've got bus data so let's update the screen. 80 | self.draw_filter() 81 | else: 82 | # No bus data so notify the user. 83 | lb = Label(text="Error fetching data...") 84 | self.ids.bx_filter.clear_widgets() 85 | self.ids.bx_filter.add_widget(lb) 86 | 87 | def draw_filter(self): 88 | """Create a list of toggle buttons to allow user to show which buses 89 | should be shown on the screen. 90 | """ 91 | # If we've got no filter then we need to set it up: 92 | if self.filters is None: 93 | 94 | # Clear the previous filter 95 | self.ids.bx_filter.clear_widgets() 96 | 97 | # Get the list of unique bus routes and apply a natural sort. 98 | routes = sorted(set([x["route"] for x in self.buses]), 99 | key=natural_sort_key) 100 | 101 | # Create a toggle button for each route and set it as enabled 102 | # for now. 103 | for route in routes: 104 | tb = ToggleButton(text=route, state="down") 105 | tb.bind(state=self.toggled) 106 | self.ids.bx_filter.add_widget(tb) 107 | 108 | # Run the "toggled" method now as this updates which buses are shown. 109 | self.toggled(None, None) 110 | 111 | def draw_buses(self): 112 | """Adds the buses to the main screen.""" 113 | # Clear the screen of any buses. 114 | self.ids.bx_buses.clear_widgets() 115 | 116 | # Get a list of just those buses who are included in the filter. 117 | buses = [b for b in self.buses if b["route"] in self.filters] 118 | 119 | # Work out the height needed to display all the buses 120 | # (we need this for the StackLayout) 121 | h = (len(buses) * 30) 122 | 123 | # Create a StackLayout and ScrollView 124 | sl = StackLayout(orientation="tb-lr", height=h, size_hint=(1, None)) 125 | sv = ScrollView(size_hint=(1, 1)) 126 | sv.add_widget(sl) 127 | self.ids.bx_buses.add_widget(sv) 128 | 129 | # Loop over the buses, create a LondonBus object and add it to the 130 | # StackLayout 131 | for bus in buses: 132 | bs = LondonBus(bus=bus) 133 | sl.add_widget(bs) 134 | 135 | def toggled(self, instance, value): 136 | """Updates self.filters to include only those bus routes whose 137 | toggle buttons are set. 138 | """ 139 | self.filters = [tb.text for tb in self.ids.bx_filter.children 140 | if tb.state == "down"] 141 | self.draw_buses() 142 | 143 | 144 | class LondonBusScreen(Screen): 145 | """Base screen object for London Buses. 146 | 147 | Has a screenmanager to hold screens for specific bus stops. 148 | """ 149 | def __init__(self, **kwargs): 150 | super(LondonBusScreen, self).__init__(**kwargs) 151 | self.params = kwargs["params"] 152 | self.stops = self.params["stops"] 153 | self.flt = self.ids.lbus_float 154 | self.flt.remove_widget(self.ids.lbus_base_box) 155 | self.scrmgr = self.ids.lbus_scrmgr 156 | self.running = False 157 | self.scrid = 0 158 | self.myscreens = [str(x["stopid"]) for x in self.stops] 159 | 160 | def on_enter(self): 161 | # If this is the first time we've come across the screen then we need 162 | # to set up a sub-screen for each bus stop. 163 | if not self.running: 164 | for stop in self.stops: 165 | nm = str(stop["stopid"]) 166 | self.scrmgr.add_widget(LondonBusStop(stop=stop, name=nm)) 167 | self.running = True 168 | 169 | else: 170 | # Fixes bug where nested screens don't have "on_enter" or 171 | # "on_leave" methods called. 172 | for c in self.scrmgr.children: 173 | if c.name == self.scrmgr.current: 174 | c.on_enter() 175 | 176 | def on_leave(self): 177 | # Fixes bug where nested screens don't have "on_enter" or 178 | # "on_leave" methods called. 179 | for c in self.scrmgr.children: 180 | if c.name == self.scrmgr.current: 181 | c.on_leave() 182 | 183 | def next_screen(self, rev=True): 184 | a = self.myscreens 185 | n = -1 if rev else 1 186 | self.scrid = (self.scrid + n) % len(a) 187 | self.scrmgr.transition.direction = "up" if rev else "down" 188 | self.scrmgr.current = a[self.scrid] 189 | -------------------------------------------------------------------------------- /screens/mythtv/README: -------------------------------------------------------------------------------- 1 | MythTV Status 2 | ------------- 3 | 4 | This plugin has three dependencies: 5 | - MytvTV python bindings 6 | - MySQLdb 7 | - lxml 8 | 9 | You can google instructions for the last two. However, for the first one, you just need to copy the MythTV folder from "/usr/lib/python2.7/dist-packages/" on your MythTV backend. 10 | 11 | You also need to copy config.xml from your myth box (found at ~/.mythtv) to a ~/.mythtv folder on your raspberry pi. 12 | -------------------------------------------------------------------------------- /screens/mythtv/cache/cache.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/mythtv/cache/cache.json -------------------------------------------------------------------------------- /screens/mythtv/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "MythTVScreen", 3 | "kv": "mythtv.kv", 4 | "dependencies": ["MythTV", "MySQLdb", "lxml"], 5 | "enabled": false 6 | } 7 | -------------------------------------------------------------------------------- /screens/mythtv/mythtv.kv: -------------------------------------------------------------------------------- 1 | 2 | text: root.rec_date 3 | height: 25 4 | size_hint_y: None 5 | halign: "left" 6 | padding: 10, 5 7 | 8 | 9 | orientation: "horizontal" 10 | height: max(mrec_desc.texture_size[1], mrec_time.texture_size[1], mrec_title.texture_size[1]) 11 | size_hint_y: None 12 | 13 | BGLabel: 14 | id: mrec_time 15 | size_hint: 0.2, None 16 | text_size: self.width, None 17 | height: max(self.texture_size[1], mrec_title.texture_size[1], mrec_desc.texture_size[1]) 18 | text: root.rec["time"] 19 | bgcolour: root.bg 20 | padding: (2, 2) 21 | 22 | BGLabel: 23 | id: mrec_title 24 | size_hint: 0.3, None 25 | text_size: self.width, None 26 | height: max(self.texture_size[1], mrec_time.texture_size[1], mrec_desc.texture_size[1]) 27 | text: "{}\n{}".format(root.rec["title"], root.rec["subtitle"]) 28 | halign: "left" 29 | bgcolour: root.bg 30 | padding: (2, 2) 31 | 32 | BGLabel: 33 | id: mrec_desc 34 | text: root.rec["desc"] 35 | size_hint: 0.5, None 36 | text_size: self.width, None 37 | height: max(self.texture_size[1], mrec_time.texture_size[1], mrec_title.texture_size[1]) 38 | halign: "left" 39 | bgcolour: root.bg 40 | padding: 10, 10 41 | 42 | 43 | 44 | BoxLayout: 45 | orientation: "vertical" 46 | 47 | Label: 48 | size_hint_y: 0.15 49 | text: "Upcoming TV Recordings" 50 | 51 | ScrollView: 52 | id: myth_scroll 53 | size_hint: 1, 1 54 | 55 | BoxLayout: 56 | orientation: "horizontal" 57 | size_hint_y: 0.1 58 | 59 | Label: 60 | halign: "left" 61 | text: "Backend is {}".format("online" if root.backendonline else "offline") 62 | padding: 5, 0 63 | font_size: 11 64 | 65 | Label: 66 | halign: "right" 67 | text: "Recording" if root.isrecording else "" 68 | padding: 5, 0 69 | font_size: 11 70 | -------------------------------------------------------------------------------- /screens/mythtv/screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import datetime as dt 4 | import json 5 | from itertools import groupby 6 | 7 | from kivy.properties import (StringProperty, 8 | DictProperty, 9 | ListProperty, 10 | BooleanProperty) 11 | from kivy.uix.boxlayout import BoxLayout 12 | from kivy.uix.screenmanager import Screen 13 | from kivy.uix.gridlayout import GridLayout 14 | 15 | from core.bglabel import BGLabel 16 | 17 | from MythTV import MythBE 18 | 19 | EPOCH = dt.datetime(1970, 1, 1) 20 | 21 | 22 | class MythRecording(BoxLayout): 23 | """Widget class for displaying information about upcoming recordings.""" 24 | rec = DictProperty({}) 25 | bg = ListProperty([0.1, 0.15, 0.15, 1]) 26 | 27 | def __init__(self, **kwargs): 28 | super(MythRecording, self).__init__(**kwargs) 29 | self.rec = kwargs["rec"] 30 | 31 | 32 | class MythRecordingHeader(BGLabel): 33 | """Widget class for grouping recordings by day.""" 34 | rec_date = StringProperty("") 35 | 36 | def __init__(self, **kwargs): 37 | super(MythRecordingHeader, self).__init__(**kwargs) 38 | self.bgcolour = [0.1, 0.1, 0.4, 1] 39 | self.rec_date = kwargs["rec_date"] 40 | 41 | 42 | class MythTVScreen(Screen): 43 | """Main screen class for MythTV schedule. 44 | 45 | Screen attempts to connect to MythTV backend and retrieve list of 46 | upcoming recordings and display this. 47 | 48 | Data is cached so that information can still be viewed even if backend 49 | is offline (e.g. for power saving purposes). 50 | """ 51 | backendonline = BooleanProperty(False) 52 | isrecording = BooleanProperty(False) 53 | 54 | def __init__(self, **kwargs): 55 | super(MythTVScreen, self).__init__(**kwargs) 56 | 57 | # Get the path for the folder 58 | scr = sys.modules[self.__class__.__module__].__file__ 59 | 60 | # Create variable to retain path to our cache fie 61 | self.screendir = os.path.dirname(scr) 62 | self.cacheFile = os.path.join(self.screendir, "cache", "cache.json") 63 | 64 | # Some other useful variable 65 | self.running = False 66 | self.rec_timer = None 67 | self.status_timer = None 68 | self.be = None 69 | self.recs = None 70 | 71 | def on_enter(self): 72 | # We only update when we enter the screen. No need for regular updates. 73 | self.getRecordings() 74 | self.drawScreen() 75 | self.checkRecordingStatus() 76 | 77 | def on_leave(self): 78 | pass 79 | 80 | def cacheRecs(self, recs): 81 | """Method to save local copy of recordings. Backend may not be online 82 | all the time so a cache enables us to display recordings if if we 83 | can't poll the server for an update. 84 | """ 85 | with open(self.cacheFile, 'w') as outfile: 86 | json.dump(recs, outfile) 87 | 88 | def loadCache(self): 89 | """Retrieves cached recorings and returns as a python list object.""" 90 | try: 91 | raw = open(self.cacheFile, 'r') 92 | recs = json.load(raw) 93 | except: 94 | recs = [] 95 | 96 | return recs 97 | 98 | def recs_to_dict(self, uprecs): 99 | """Converts the MythTV upcoming recording iterator into a list of 100 | dict objects. 101 | """ 102 | raw_recs = [] 103 | recs = [] 104 | 105 | # Turn the response into a dict object and add to our list of recorings 106 | for r in uprecs: 107 | rec = {} 108 | st = r.starttime 109 | et = r.endtime 110 | rec["title"] = r.title 111 | rec["subtitle"] = r.subtitle if r.subtitle else "" 112 | day = dt.datetime(st.year, st.month, st.day) 113 | rec["day"] = (day - EPOCH).total_seconds() 114 | rec["time"] = "{} - {}".format(st.strftime("%H:%M"), 115 | et.strftime("%H:%M")) 116 | rec["timestamp"] = (st - EPOCH).total_seconds() 117 | rec["desc"] = r.description 118 | raw_recs.append(rec) 119 | 120 | # Group the recordings by day (so we can print a header) 121 | for k, g in groupby(raw_recs, lambda x: x["day"]): 122 | recs.append((k, list(g))) 123 | 124 | return recs 125 | 126 | def getRecordings(self): 127 | """Attempts to connect to MythTV backend and retrieve recordings.""" 128 | try: 129 | # If we can connect then get recordings and save a local cache. 130 | self.be = MythBE() 131 | uprecs = self.be.getUpcomingRecordings() 132 | self.recs = self.recs_to_dict(uprecs) 133 | self.cacheRecs(self.recs) 134 | self.backendonline = True 135 | except: 136 | # Can't connect so we need to set variables accordinly and try 137 | # to load data from the cache. 138 | self.be = None 139 | self.recs = self.loadCache() 140 | self.backendonline = False 141 | 142 | def checkRecordingStatus(self): 143 | """Checks whether the backend is currently recording.""" 144 | try: 145 | recbe = MythBE() 146 | for recorder in recbe.getRecorderList(): 147 | if recbe.isRecording(recorder): 148 | self.isrecording = True 149 | break 150 | except: 151 | # If we can't connect to it then it can't be recording. 152 | self.isrecording = False 153 | 154 | def drawScreen(self): 155 | """Main method for rendering screen. 156 | 157 | If there is recording data (live or cached) then is laid out in a 158 | scroll view. 159 | 160 | If not, the user is notified that the backend is unreachable. 161 | """ 162 | sv = self.ids.myth_scroll 163 | sv.clear_widgets() 164 | 165 | if self.recs: 166 | # Create a child widget to hold the recordings. 167 | self.sl = GridLayout(cols=1, size_hint=(1, None), spacing=2) 168 | self.sl.bind(minimum_height=self.sl.setter('height')) 169 | 170 | # Loop over the list of recordings. 171 | for rec in self.recs: 172 | 173 | # These are grouped by day so we need a header 174 | day = dt.timedelta(0, rec[0]) + EPOCH 175 | mrh = MythRecordingHeader(rec_date=day.strftime("%A %d %B")) 176 | self.sl.add_widget(mrh) 177 | 178 | # Then we loop over the recordings scheduled for that day 179 | for r in rec[1]: 180 | # and add them to the display. 181 | mr = MythRecording(rec=r) 182 | self.sl.add_widget(mr) 183 | 184 | sv.add_widget(self.sl) 185 | 186 | else: 187 | lb = Label(text="Backend is unreachable and there is no cached" 188 | " information") 189 | sv.add_widget(lb) 190 | -------------------------------------------------------------------------------- /screens/photoalbum/README: -------------------------------------------------------------------------------- 1 | Photo album 2 | ----------- 3 | 4 | Set up: 5 | 6 | Edit the conf.json file as follows 7 | 8 | - "duration": The number of seconds to show the photo 9 | - "extensions": A list of extensions that will be included when searching for photos. 10 | - "folders": A list of folders to search for photos (not recursive) 11 | -------------------------------------------------------------------------------- /screens/photoalbum/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "PhotoAlbumScreen", 3 | "kv": "photos.kv", 4 | "dependencies": [], 5 | "enabled": false, 6 | "params": 7 | { 8 | "duration": 5, 9 | "extensions": ["jpg", "png"], 10 | "folders": 11 | [ 12 | "/home/pi/Pictures", 13 | "/home/pi/Desktop" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /screens/photoalbum/photos.kv: -------------------------------------------------------------------------------- 1 | #:import FadeTransition kivy.uix.screenmanager.FadeTransition 2 | 3 | : 4 | photoscreen: photoscreen 5 | 6 | ScreenManager: 7 | id: photoscreen 8 | transition: FadeTransition(duration=1) 9 | 10 | 11 | AsyncImage: 12 | source: root.photo_path 13 | 14 | 15 | Label: 16 | text: "Retrieving list of photos" 17 | -------------------------------------------------------------------------------- /screens/photoalbum/screen.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | 4 | from kivy.clock import Clock 5 | from kivy.properties import (ObjectProperty, 6 | StringProperty, 7 | BoundedNumericProperty) 8 | from kivy.uix.label import Label 9 | from kivy.uix.screenmanager import Screen 10 | 11 | 12 | class Photo(Screen): 13 | """Screen object to display a photo.""" 14 | photo_path = StringProperty("") 15 | 16 | def __init__(self, **kwargs): 17 | super(Photo, self).__init__(**kwargs) 18 | self.photo_path = self.name 19 | 20 | 21 | class PhotoLoading(Screen): 22 | """Holding screen to display while the screen retrieves the list of 23 | photos. 24 | """ 25 | pass 26 | 27 | 28 | class PhotoAlbumScreen(Screen): 29 | """Base screen to run the photo album.""" 30 | 31 | # Reference to the screen manager 32 | photoscreen = ObjectProperty(None) 33 | 34 | # Value for the screen display time 35 | photoduration = BoundedNumericProperty(5, min=2, max=60, errorvalue=5) 36 | 37 | def __init__(self, **kwargs): 38 | super(PhotoAlbumScreen, self).__init__(**kwargs) 39 | 40 | # Get the user's preferences 41 | self.folders = kwargs["params"]["folders"] 42 | self.exts = kwargs["params"]["extensions"] 43 | self.photoduration = kwargs["params"]["duration"] 44 | 45 | # Initialise some useful variables 46 | self.running = False 47 | self.photos = [] 48 | self.timer = None 49 | self.oldPhoto = None 50 | self.photoindex = 0 51 | 52 | def on_enter(self): 53 | 54 | if not self.running: 55 | 56 | # The screen hasn't been run before so let's tell the user 57 | # that we need to get the photos 58 | self.loading = PhotoLoading(name="loading") 59 | self.photoscreen.add_widget(self.loading) 60 | self.photoscreen.current = "loading" 61 | 62 | # Retrieve photos 63 | Clock.schedule_once(self.getPhotos, 0.5) 64 | 65 | else: 66 | # We've been here before so just show the photos 67 | self.timer = Clock.schedule_interval(self.showPhoto, 68 | self.photoduration) 69 | 70 | def on_leave(self): 71 | 72 | # We can stop looping over photos 73 | Clock.unschedule(self.timer) 74 | 75 | def getPhotos(self, *args): 76 | """Method to retrieve list of photos based on user's preferences.""" 77 | 78 | # Get a list of extensions. Assumes all caps or all lower case. 79 | exts = [] 80 | for ext in ([x.upper(), x.lower()] for x in self.exts): 81 | exts.extend(ext) 82 | 83 | # Loop over the folders 84 | for folder in self.folders: 85 | 86 | # Look for each extension 87 | for ext in exts: 88 | 89 | # Get the photos 90 | photos = glob(os.path.join(folder, "*.{}".format(ext))) 91 | 92 | # Add to our master list 93 | self.photos.extend(photos) 94 | 95 | # Put the photos in order 96 | self.photos.sort() 97 | 98 | # We've got the photos so we can set the running flag 99 | self.running = True 100 | 101 | # and start the timer 102 | self.timer = Clock.schedule_interval(self.showPhoto, 103 | self.photoduration) 104 | 105 | # Show the first photo 106 | self.showPhoto() 107 | 108 | def showPhoto(self, *args): 109 | """Method to update the currently displayed photo.""" 110 | 111 | # Get the current photo 112 | photo = self.photos[self.photoindex] 113 | 114 | # Create a screen pbject to show that photo 115 | scr = Photo(name=photo) 116 | 117 | # Add it to our screenmanager and display it 118 | self.photoscreen.add_widget(scr) 119 | self.photoscreen.current = photo 120 | 121 | # If we've got an old photo 122 | if self.oldPhoto: 123 | 124 | # We can unload it 125 | self.photoscreen.remove_widget(self.oldPhoto) 126 | 127 | # Create reference to the new photo 128 | self.oldPhoto = scr 129 | 130 | # Increase our index for the next photo 131 | self.photoindex = (self.photoindex + 1) % len(self.photos) 132 | -------------------------------------------------------------------------------- /screens/pong/README: -------------------------------------------------------------------------------- 1 | PONG! 2 | 3 | Self explanatory really... 4 | 5 | Set the winning score total in the config file. 6 | 7 | Once the game starts, the screen will lock so you can't switch between screens. The screen only unlocks when a game is over. 8 | -------------------------------------------------------------------------------- /screens/pong/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "PongScreen", 3 | "kv": "pong.kv", 4 | "dependencies": [], 5 | "enabled": false, 6 | "params": 7 | {"winningscore": 5} 8 | 9 | } 10 | -------------------------------------------------------------------------------- /screens/pong/pong.kv: -------------------------------------------------------------------------------- 1 | : 2 | size: 400, 75 3 | size_hint: None, None 4 | font_size: 50 5 | 6 | : 7 | size: 50, 50 8 | size_hint: None, None 9 | canvas: 10 | Ellipse: 11 | pos: self.pos 12 | size: self.size 13 | 14 | : 15 | size: 25, 200 16 | size_hint: None, None 17 | canvas: 18 | Rectangle: 19 | pos:self.pos 20 | size:self.size 21 | 22 | : 23 | ball: pong_ball 24 | player1: player_left 25 | player2: player_right 26 | pongfloat: pongfloat 27 | 28 | FloatLayout: 29 | 30 | id: pongfloat 31 | 32 | canvas: 33 | Rectangle: 34 | pos: self.center_x-5, 0 35 | size: 10, self.height 36 | 37 | Label: 38 | 39 | size: 300, 40 40 | size_hint: None, None 41 | font_size: 70 42 | center_x: root.width / 4 43 | top: root.top - 40 44 | text: str(root.player1.score) 45 | 46 | Label: 47 | 48 | size: 300, 40 49 | size_hint: None, None 50 | font_size: 70 51 | center_x: root.width * 3 / 4 52 | top: root.top - 40 53 | text: str(root.player2.score) 54 | 55 | PongBall: 56 | id: pong_ball 57 | center: self.parent.center 58 | 59 | PongPaddle: 60 | id: player_left 61 | x: root.x + 50 62 | center_y: root.center_y 63 | 64 | PongPaddle: 65 | id: player_right 66 | x: root.width-self.width - 50 67 | center_y: root.center_y 68 | -------------------------------------------------------------------------------- /screens/pong/screen.py: -------------------------------------------------------------------------------- 1 | '''Pong Game screen for the Raspberry Pi Information Screen. 2 | 3 | Pong Game code is based on the code on the Kivy website: 4 | http://kivy.org/docs/tutorials/pong.html 5 | ''' 6 | from time import sleep 7 | 8 | from kivy.app import App 9 | from kivy.uix.widget import Widget 10 | from kivy.properties import NumericProperty, ReferenceListProperty,\ 11 | ObjectProperty 12 | from kivy.vector import Vector 13 | from kivy.clock import Clock 14 | from random import randint 15 | from kivy.uix.screenmanager import Screen 16 | from kivy.uix.label import Label 17 | from kivy.uix.floatlayout import FloatLayout 18 | from kivy.app import App 19 | 20 | from core.bglabel import BGLabelButton 21 | 22 | 23 | class WinLabel(BGLabelButton): 24 | pass 25 | 26 | 27 | class PongPaddle(Widget): 28 | score = NumericProperty(0) 29 | 30 | def bounce_ball(self, ball): 31 | if self.collide_widget(ball): 32 | vx, vy = ball.velocity 33 | offset = (ball.center_y - self.center_y) / (self.height / 2) 34 | bounced = Vector(-1 * vx, vy) 35 | vel = bounced * 1.1 36 | ball.velocity = vel.x, vel.y + offset 37 | 38 | 39 | class PongBall(Widget): 40 | velocity_x = NumericProperty(0) 41 | velocity_y = NumericProperty(0) 42 | velocity = ReferenceListProperty(velocity_x, velocity_y) 43 | 44 | def move(self): 45 | self.pos = Vector(*self.velocity) + self.pos 46 | 47 | 48 | class PongScreen(Screen): 49 | ball = ObjectProperty(None) 50 | player1 = ObjectProperty(None) 51 | player2 = ObjectProperty(None) 52 | pongfloat = ObjectProperty(None) 53 | 54 | def __init__(self, **kwargs): 55 | super(PongScreen, self).__init__(**kwargs) 56 | try: 57 | self.winscore = int(kwargs["params"]["winningscore"]) 58 | if self.winscore < 0: 59 | self.winscore = 5 60 | except ValueError: 61 | self.winscore = 5 62 | 63 | self.speed = 8 64 | 65 | def lock(self, locked=True): 66 | app = App.get_running_app() 67 | app.base.toggle_lock(locked) 68 | 69 | def unload(self): 70 | self.lock(False) 71 | 72 | def on_enter(self): 73 | self.ball.center = self.center 74 | self.player1.center_y = self.center_y 75 | self.player2.center_y = self.center_y 76 | self.lbl = WinLabel(text="Press to start!", 77 | pos=(200, 160), 78 | bgcolour=[0, 0, 0, 0.8]) 79 | cb = lambda instance, lbl=self.lbl: self.start(lbl) 80 | self.lbl.bind(on_press=cb) 81 | self.pongfloat.add_widget(self.lbl) 82 | 83 | def start(self, lbl): 84 | self.pongfloat.remove_widget(lbl) 85 | self.serve_ball() 86 | Clock.schedule_interval(self.update, 1.0 / 30.0) 87 | self.lock(True) 88 | 89 | def restart(self, vel, lbl): 90 | self.lock(True) 91 | self.pongfloat.remove_widget(lbl) 92 | self.player1.score = 0 93 | self.player2.score = 0 94 | self.serve_ball(vel) 95 | 96 | def on_leave(self): 97 | Clock.unschedule(self.update) 98 | self.player1.score = 0 99 | self.player2.score = 0 100 | for c in self.pongfloat.children: 101 | if isinstance(c, WinLabel): 102 | self.pongfloat.remove_widget(c) 103 | 104 | def win(self, player): 105 | 106 | self.lbl = WinLabel(text="Player {} wins!".format(player), 107 | pos=(200, 160), 108 | bgcolour=[0, 0, 0, 0.8]) 109 | 110 | v = (0 - self.speed, 0) if player == 1 else (self.speed, 0) 111 | 112 | cb = lambda instance, v=v, lbl=self.lbl: self.restart(v, lbl) 113 | 114 | self.lbl.bind(on_press=cb) 115 | 116 | self.pongfloat.add_widget(self.lbl) 117 | self.ball.velocity = (0, 0) 118 | self.ball.center = self.center 119 | 120 | Clock.schedule_once(self.reset, 2) 121 | 122 | def reset(self, *args): 123 | self.lock(False) 124 | self.lbl.text = "Press to restart" 125 | 126 | def serve_ball(self, vel=None): 127 | self.ball.center = self.center 128 | if vel is None: 129 | self.ball.velocity = (self.speed, 0) 130 | else: 131 | self.ball.velocity = vel 132 | 133 | def update(self, dt): 134 | self.ball.move() 135 | 136 | # bounce of paddles 137 | self.player1.bounce_ball(self.ball) 138 | self.player2.bounce_ball(self.ball) 139 | 140 | # bounce ball off bottom or top 141 | if (self.ball.y < self.y) or (self.ball.top > self.top): 142 | self.ball.velocity_y *= -1 143 | 144 | # went of to a side to score point? 145 | if self.ball.x < self.x: 146 | self.player2.score += 1 147 | if self.player2.score == self.winscore: 148 | self.win(2) 149 | else: 150 | self.serve_ball(vel=(self.speed, 0)) 151 | if self.ball.x > self.width: 152 | self.player1.score += 1 153 | if self.player1.score == self.winscore: 154 | self.win(1) 155 | else: 156 | self.serve_ball(vel=(0 - self.speed, 0)) 157 | 158 | def on_touch_move(self, touch): 159 | if touch.x < self.width / 3: 160 | self.player1.center_y = touch.y 161 | if touch.x > self.width - self.width / 3: 162 | self.player2.center_y = touch.y 163 | -------------------------------------------------------------------------------- /screens/squeezeplayer/README: -------------------------------------------------------------------------------- 1 | Squeezeplayer Screen 2 | -------------------- 3 | 4 | Setting up: 5 | 6 | Enter the IP address of the machine running the Logitech Media Server in the conf.json file. 7 | 8 | If you've changed the default ports then you should update those values too. 9 | 10 | 11 | What the screen does: 12 | 13 | - Shows "Now Playing" information 14 | - Basic player controls 15 | - Shows playlist (including ability to select track to play) 16 | - Shows players (includes ability to select different players to control) 17 | -------------------------------------------------------------------------------- /screens/squeezeplayer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/__init__.py -------------------------------------------------------------------------------- /screens/squeezeplayer/artworkresolver.py: -------------------------------------------------------------------------------- 1 | from urllib import urlencode 2 | 3 | class ArtworkResolver(object): 4 | """Class object to help provide an easy way of obtaining a URL to a 5 | playlist item. 6 | 7 | The class is capable of working out the appropriate path depending on 8 | whether the file is remote or local. 9 | 10 | A default image path can also be provided. If none is provided, this 11 | will fall back to the LMS default image. 12 | """ 13 | def __init__(self, host="localhost", port=9000, default=None): 14 | self.host = host 15 | self.port = port 16 | 17 | # Custom plugins may use a different image format 18 | # Set up some methods to handle them 19 | self.methods = {"spotifyimage": self.__spotify_url, 20 | "imageproxy/spotify:image:": self.__spotify_url} 21 | 22 | # Set up the template for local artwork 23 | self.localart = "http://{host}:{port}/music/{coverid}/cover.jpg" 24 | 25 | # Set the default path for missing artwork 26 | if default is not None: 27 | self.default = default 28 | else: 29 | self.default = self.localart.format(host=self.host, 30 | port=self.port, 31 | coverid=0) 32 | 33 | # Prefix for remote artwork 34 | self.prefix = "http://www.mysqueezebox.com/public/imageproxy?{data}" 35 | 36 | def __getRemoteURL(self, track, size): 37 | # Check whether there's a URL for remote artworl 38 | art = track.get("artwork_url", False) 39 | 40 | # If there is, build the link. 41 | if art: 42 | for k in self.methods: 43 | if art.startswith(k): 44 | return self.methods[k](art) 45 | 46 | h, w = size 47 | data= {"h": h, 48 | "w": w, 49 | "u": art} 50 | return self.prefix.format(data=urlencode(data)) 51 | 52 | # If not, return the fallback image 53 | else: 54 | return self.default 55 | 56 | def __getLocalURL(self, track): 57 | # Check if local cover art is available 58 | coverart = track.get("coverart", False) 59 | 60 | # If so, build the link 61 | if coverart: 62 | 63 | return self.localart.format(host=self.host, 64 | port=self.port, 65 | coverid=track["coverid"]) 66 | 67 | # If not, return the fallback image 68 | else: 69 | return self.default 70 | 71 | def __spotify_url(self, art): 72 | """Spotify images (using Triode's plugin) are provided on the local 73 | server. 74 | """ 75 | return "http://{host}:{port}/{art}".format(host=self.host, 76 | port=self.port, 77 | art=art) 78 | 79 | def getURL(self, track, size=(50, 50)): 80 | """Method for generating link to artwork for the selected track. 81 | 82 | 'track' is a dict object which must contain the "remote", "coverid" 83 | and "coverart" tags as returned by the server. 84 | 85 | 'size' is an optional parameter which can be used when creting links 86 | for remotely hosted images. 87 | """ 88 | 89 | # List of required keys 90 | required = ["remote", "coverart"] 91 | 92 | # Check that we've received the right type of data 93 | if type(track) != dict: 94 | raise TypeError("track should be a dict") 95 | 96 | # Check if all the keys are present 97 | if not set(required) < set(track.keys()): 98 | raise KeyError("track should have 'remote' and" 99 | " 'coverart' keys") 100 | 101 | # Check the flags for local and remote art 102 | track["coverart"] = int(track["coverart"]) 103 | remote = int(track["remote"]) 104 | 105 | # If it's a remotely hosted file, let's get the link 106 | if remote: 107 | return self.__getRemoteURL(track, size) 108 | 109 | # or it's a local file, so let's get the link 110 | else: 111 | return self.__getLocalURL(track) 112 | -------------------------------------------------------------------------------- /screens/squeezeplayer/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "SqueezePlayerScreen", 3 | "kv": "squeezeplayer.kv", 4 | "dependencies": [], 5 | "enabled": false, 6 | "params": 7 | { 8 | "host": 9 | { 10 | "address": "192.168.0.1", 11 | "webport": 9000, 12 | "telnetport": 9090 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /screens/squeezeplayer/icons/sq_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/icons/sq_next.png -------------------------------------------------------------------------------- /screens/squeezeplayer/icons/sq_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/icons/sq_pause.png -------------------------------------------------------------------------------- /screens/squeezeplayer/icons/sq_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/icons/sq_play.png -------------------------------------------------------------------------------- /screens/squeezeplayer/icons/sq_previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/icons/sq_previous.png -------------------------------------------------------------------------------- /screens/squeezeplayer/icons/sq_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/icons/sq_stop.png -------------------------------------------------------------------------------- /screens/squeezeplayer/icons/sq_volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/squeezeplayer/icons/sq_volume.png -------------------------------------------------------------------------------- /screens/squeezeplayer/pylms/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | -------------------------------------------------------------------------------- /screens/squeezeplayer/pylms/callback_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | An asynchronous client that listens to messages broadcast by the server. 3 | 4 | The client also accepts callback functions. 5 | 6 | The client subclasses python threading so methods are built-in to the class 7 | object. 8 | """ 9 | from threading import Thread 10 | from telnetlib import IAC, NOP 11 | import socket 12 | 13 | from pylms.server import Server 14 | 15 | 16 | class CallbackServer(Server, Thread): 17 | 18 | MIXER_ALL = "mixer" 19 | VOLUME_CHANGE = "mixer volume" 20 | 21 | PLAYLIST_ALL = "playlist" 22 | PLAY_PAUSE = "playlist pause" 23 | PLAY = "playlist pause 0" 24 | PAUSE = "playlist pause 1" 25 | PLAYLIST_OPEN = "playlist open" 26 | PLAYLIST_CHANGE_TRACK = "playlist newsong" 27 | PLAYLIST_LOAD_TRACKS = "playlist loadtracks" 28 | PLAYLIST_ADD_TRACKS = "playlist addtracks" 29 | PLAYLIST_LOADED = "playlist load_done" 30 | PLAYLIST_REMOVE = "playlist delete" 31 | PLAYLIST_CLEAR = "playlist clear" 32 | PLAYLIST_CHANGED = [PLAYLIST_LOAD_TRACKS, 33 | PLAYLIST_LOADED, 34 | PLAYLIST_ADD_TRACKS, 35 | PLAYLIST_REMOVE, 36 | PLAYLIST_CLEAR] 37 | 38 | CLIENT_ALL = "client" 39 | CLIENT_NEW = "client new" 40 | CLIENT_DISCONNECT = "client disconnect" 41 | CLIENT_RECONNECT = "client reconnect" 42 | 43 | SYNC = "sync" 44 | 45 | def __init__(self, **kwargs): 46 | super(CallbackServer, self).__init__(**kwargs) 47 | self.callbacks = {} 48 | self.notifications = [] 49 | self.abort = False 50 | self.ending = "\n".encode(self.charset) 51 | 52 | def add_callback(self, event, callback): 53 | """Add a callback. 54 | 55 | Takes two parameter: 56 | event: string of single notification or list of notifications 57 | callback: function to be run if server sends matching notification 58 | """ 59 | if type(event) == list: 60 | for ev in event: 61 | self.__add_callback(ev, callback) 62 | 63 | else: 64 | self.__add_callback(event, callback) 65 | 66 | def __add_callback(self, event, callback): 67 | self.callbacks[event] = callback 68 | notification = event.split(" ")[0] 69 | if notification not in self.notifications: 70 | self.notifications.append(notification) 71 | 72 | def remove_callback(self, event): 73 | """Remove a callback. 74 | 75 | Takes one parameter: 76 | event: string of single notification or list of notifications 77 | """ 78 | if type(event) == list: 79 | for ev in event: 80 | self.__remove_callback(ev) 81 | 82 | else: 83 | self.__remove_callback(event) 84 | 85 | def __remove_callback(self, event): 86 | del self.callbacks[event] 87 | 88 | def check_event(self, event): 89 | """Checks whether any of the requested notification types match the 90 | received notification. If there's a match, we run the requested 91 | callback function passing the notification as the only parameter. 92 | """ 93 | for cb in self.callbacks: 94 | if cb in event: 95 | self.callbacks[cb](self.unquote(event)) 96 | break 97 | 98 | def check_connection(self): 99 | """Method to check whether we can still connect to the server. 100 | 101 | Sets the flag to stop the server if no collection is available. 102 | """ 103 | # Create a socket object 104 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 105 | 106 | # Set a timeout - we don't want this to block unnecessarily 107 | s.settimeout(2) 108 | 109 | try: 110 | # Try to connect 111 | s.connect((self.hostname, self.port)) 112 | 113 | except socket.error: 114 | # We can't connect so stop the server 115 | self.abort = True 116 | 117 | # Close our socket object 118 | s.close() 119 | 120 | def run(self): 121 | self.connect() 122 | 123 | # If we've already defined callbacks then we know which events we're 124 | # listening out for 125 | if self.notifications: 126 | nots = ",".join(self.notifications) 127 | self.request("subscribe {}".format(nots)) 128 | 129 | # If not, let's just listen for everything. 130 | else: 131 | self.request("listen") 132 | 133 | while not self.abort: 134 | try: 135 | # Include a timeout to stop blocking if no server 136 | data = self.telnet.read_until(self.ending, timeout=1)[:-1] 137 | 138 | # We've got a notification, so let's see if it's one we're 139 | # watching. 140 | if data: 141 | self.check_event(data) 142 | 143 | # Server is unavailable so exit gracefully 144 | except EOFError: 145 | self.abort = True 146 | -------------------------------------------------------------------------------- /screens/squeezeplayer/pylms/client.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Ben Weiner, http://github.com/readingtype' 2 | 3 | """ 4 | A listening Client derived from Server by Ben Weiner, https://github.com/readingtype 5 | Start the Client instance in a thread so you can do other stuff while it is running. 6 | """ 7 | 8 | import telnetlib 9 | import urllib 10 | import pylms 11 | from pylms.server import Server 12 | from pylms.player import Player 13 | 14 | class Client(Server): 15 | 16 | def start(self): 17 | self.connect() 18 | self.request("listen") 19 | 20 | while True: 21 | received = self.telnet.read_until("\n".encode(self.charset))[:-1] 22 | if received: 23 | status = self.request_with_results(command_string=None, received=received) 24 | 25 | def telnet_connect(self): 26 | """ 27 | Stay connected forever 28 | """ 29 | self.telnet = telnetlib.Telnet(self.hostname, self.port, timeout=None) 30 | 31 | def update(self, data): 32 | # overload this in your instance to do stuff 33 | print "update: %s" % data 34 | -------------------------------------------------------------------------------- /screens/squeezeplayer/pylms/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from pylms import server 4 | 5 | s = server.Server("10.0.2.10") 6 | s.connect() 7 | 8 | print(s.get_players()) 9 | 10 | p = s.get_player("Lounge") 11 | p.set_volume(10) 12 | 13 | r = s.request("songinfo 0 100 track_id:94") 14 | print(r) 15 | 16 | r = s.request("trackstat getrating 1019") 17 | print(r) 18 | -------------------------------------------------------------------------------- /screens/squeezeplayer/pylms/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def clean_command(value): 4 | """ Strips whitespace, underscores dashes and lowercases value """ 5 | return value.strip().lower().replace("_", "").replace("-", "")\ 6 | .replace(" ", "") 7 | -------------------------------------------------------------------------------- /screens/template/README: -------------------------------------------------------------------------------- 1 | A plugin has the following structure 2 | 3 | /FolderName (Must be unique) 4 | |--> conf.json 5 | |--> screen.py 6 | |--> NAME.kv 7 | 8 | 1) FolderName 9 | 10 | Must be unique as this is the identifier used by the master script to handle the different screens. 11 | 12 | 2) conf.json 13 | 14 | File must have this name. 15 | 16 | JSON formatted file. Has two mandatory parameters: 17 | "screen" - the class name of the main screen for the plugin. 18 | "kv" - the name of the kv file (including extension) which contains the layout for your plugin. 19 | 20 | Optional parameters: 21 | "enabled" - plugins can be disabled by setting this to false. 22 | "params" - any specific parameters to be passed to your plugin 23 | "dependencies" - list any non-standard libraries that are needed to run your plugin. If these aren't installed then the user will be notified. 24 | 25 | 3) screen.py 26 | 27 | File must have this name. 28 | 29 | Main python file for your plugin. Must have a class definition matching the "screen" parameter in the conf.json file. 30 | 31 | The class must inherit the Kivy Screen class. 32 | 33 | 4) NAME.kv 34 | 35 | This file can be called anything you like as long as it's name matches the name set in the conf.json file. 36 | -------------------------------------------------------------------------------- /screens/template/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "DummyScreen", 3 | "kv": "dummy.kv", 4 | "dependencies": [], 5 | "enabled": false 6 | } 7 | -------------------------------------------------------------------------------- /screens/template/dummy.kv: -------------------------------------------------------------------------------- 1 | : 2 | Label: 3 | pos: 0, 0 4 | text: "This is a test screen." 5 | -------------------------------------------------------------------------------- /screens/template/screen.py: -------------------------------------------------------------------------------- 1 | from kivy.uix.label import Label 2 | from kivy.uix.screenmanager import Screen 3 | 4 | class DummyScreen(Screen): 5 | def __init__(self, **kwargs): 6 | super(DummyScreen, self).__init__(**kwargs) 7 | -------------------------------------------------------------------------------- /screens/tides/README: -------------------------------------------------------------------------------- 1 | tides information 2 | ----------------- 3 | 4 | API Key: 5 | 6 | This plugin requires you to register for an API ley from worldtides. 7 | 8 | See: http://www.wolrdtides.info/ 9 | 10 | Configuring locations: 11 | 12 | To set up location you need to enter the longitude and latitude 13 | of the location you are interested in. 14 | -------------------------------------------------------------------------------- /screens/tides/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "TidesScreen", 3 | "kv": "tides.kv", 4 | "dependencies": ["requests", "dateutil", "pytz"], 5 | "enabled": true, 6 | "params": { 7 | "language": "english", 8 | "key": "YOUR_API_KEY_HERE", 9 | "location": 10 | {"name": "Escalles", 11 | "coords": {"lat": 50.921650, "lon": 1.701273}} 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /screens/tides/images/escalles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/tides/images/escalles.jpg -------------------------------------------------------------------------------- /screens/tides/tides.kv: -------------------------------------------------------------------------------- 1 | : 2 | width: 150 3 | size_hint_x: None 4 | orientation: "vertical" 5 | canvas.before: 6 | Color: 7 | rgba: 0.1, 0.1, 0.1, 0.2 8 | Rectangle: 9 | pos: self.pos 10 | size: self.size 11 | Label: 12 | text: root.desc 13 | valign: "middle" 14 | 15 | : 16 | canvas.before: 17 | Rectangle: 18 | pos: self.pos 19 | size: self.size 20 | source: 'screens/tides/images/escalles.jpg' 21 | FloatLayout: 22 | id: tides_float 23 | 24 | BoxLayout: 25 | id: tides_base_box 26 | Label: 27 | id: tides_lbl_load 28 | text: "Loading tides..." 29 | 30 | ScreenManager: 31 | id: tides_scrmgr 32 | auto_bring_to_front: False 33 | 34 | : 35 | BoxLayout: 36 | orientation: "vertical" 37 | Label: 38 | text: "[size=20][color=#202020]{name:s}[/color][/size]".format(**root.location) 39 | markup: True 40 | text_size: self.size 41 | valign: "top" 42 | halign: "center" 43 | padding_y: 5 44 | BoxLayout: 45 | orientation: "horizontal" 46 | Label: 47 | text: "[color=#202020][size=60][b]{type_i18n:s} {h:02d}:{m:02d}[/b][/size][/color]".format(**root.prev_t) 48 | markup: True 49 | Label: 50 | text: "[color=#202020][size=60][b]{type_i18n:s} {h:02d}:{m:02d}[/b][/size][/color]".format(**root.next_t) 51 | markup: True 52 | Label: 53 | text: "[size=50]{h:02d}:{m:02d}[/size][size=30] {s:02d}[/size]".format(**root.timedata) 54 | markup: True 55 | valign: "top" 56 | halign: "center" 57 | text_size: self.size 58 | BoxLayout: 59 | padding: 5, 5 60 | id: tides_list 61 | orientation: "horizontal" 62 | size_hint_y: 1 63 | BoxLayout: 64 | orientation: "horizontal" 65 | padding: 5 66 | Label: 67 | text: "[color=#888888]Photo by Éric Leblond[/color]" 68 | text_size: self.size 69 | valign: "bottom" 70 | halign: "left" 71 | markup: True 72 | Label: 73 | text: "[color=#888888]Data by Worldtides[/color]" 74 | text_size: self.size 75 | valign: "bottom" 76 | halign: "right" 77 | markup: True 78 | 79 | -------------------------------------------------------------------------------- /screens/trains/README: -------------------------------------------------------------------------------- 1 | UK Train Time Lookup 2 | -------------------- 3 | 4 | Customise the plugin by adding the journeys you want. 5 | 6 | Format is as follows: 7 | 8 | {"description": "Description of your journey", 9 | "from": "3 letter station code for starting station", 10 | "to": "3 letter station code for destination"} 11 | -------------------------------------------------------------------------------- /screens/trains/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "TrainScreen", 3 | "kv": "trains.kv", 4 | "enabled": false, 5 | "dependencies": ["requests", "BeautifulSoup"], 6 | "params": { 7 | "journeys": [ 8 | {"description": "London Victoria to Clapham Junction", 9 | "from": "VIC", 10 | "to": "CLJ"}, 11 | {"description": "London Kings Cross to Edinburgh", 12 | "from": "WAT", 13 | "to": "EDB"} 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /screens/trains/nationalrail.py: -------------------------------------------------------------------------------- 1 | """This script demonstrates how to use BeautifulSoup to retrieve information 2 | from a website and turn it into a structure that can then be used by other 3 | python codes. 4 | 5 | Please note, scraping data from the National Rail website is against their 6 | terms and conditions. As such, this code should not be incorporated into any 7 | projects. 8 | """ 9 | 10 | # We need the datetime module because the timetable lookup requires a time 11 | # We can therefore insert the current time to get details on the next trains 12 | from datetime import datetime 13 | 14 | # Simple way of submitting web requests (easier to use than urllib2) 15 | import requests 16 | 17 | # BeautifulSoup is the tool we'll use for scraping the pages 18 | from BeautifulSoup import BeautifulSoup 19 | 20 | # Regular expressions will be needed to match certain classes and manipulate 21 | # some data we get back. 22 | import re 23 | 24 | # Base web address of the data that we want 25 | NR_BASE = "http://ojp.nationalrail.co.uk/service/timesandfares/" 26 | # These are the bits that will change based on what information we want to get 27 | # start: 3 letter code of starting station 28 | # dest: 3 letter code of destination 29 | # dep_day: day of travel in DDMMYY format, or "today" 30 | # dep_time: time of travel in HHMM format 31 | # direction: Are we searching by time of departure or arrival? 32 | SUFFIX = "{start}/{dest}/{dep_day}/{dep_time}/{direction}" 33 | 34 | 35 | # All the methods beginning with "__" are just the "behind the scenes" working 36 | # and are "hidden" from the user. 37 | 38 | 39 | # Simple method to submit web request 40 | def __getPage(url): 41 | r = requests.get(url) 42 | if r.status_code == 200: 43 | return r.text 44 | else: 45 | return None 46 | 47 | 48 | # Method to parse the page 49 | def __parsePage(page): 50 | # Stop if we didn't get any info back from the web request 51 | if page is None: 52 | return None 53 | 54 | else: 55 | # Send the web page to BeautifulSoup 56 | raw = BeautifulSoup(page) 57 | try: 58 | # We're using looking for table rows that contain "mtx" in the 59 | # class tag. We need to use regex here as there may be other 60 | # strings in the tag too. e.g. '' 61 | re_mtx = re.compile(r'\bmtx\b') 62 | # the findAll method can then be used to find every instance of 63 | # a match. Returns a list of matching objects. 64 | mtx = raw.findAll("tr", {"class": re_mtx}) 65 | # to avoid too much indentaton we'll handle the parsing of these 66 | # objects in a new method 67 | return __getTrains(mtx) 68 | except: 69 | raise 70 | 71 | 72 | def __getTrains(mtx): 73 | 74 | # Create an empty list to store the trains in 75 | trains = [] 76 | 77 | # Nested function (so it's not available outside of __getTrains) 78 | def txt(train, tclass): 79 | try: 80 | # This is just be being lazy to avoid repeating the whole 81 | # find line lots of times! 82 | return train.find("td", {"class": tclass}).text.strip() 83 | except: 84 | return None 85 | 86 | # Another nested function... 87 | def status(train): 88 | 89 | # The status box is a bit different to we need to parse this 90 | # differently. Let's look to see if there is an "on-time" status 91 | if train.find("td", 92 | {"class": re.compile(r"\bjourney-status-on-time\b")}): 93 | return "On Time" 94 | else: 95 | return txt(train, "status") 96 | 97 | # Loop over the matching trains 98 | for train in mtx: 99 | 100 | # Create a blank dictionary instance for the train 101 | t = {} 102 | 103 | # Get details of departing station 104 | # Some stations have this format "Long name [CODE] Platoform" 105 | # We don't need the [CODE] so let's use regex to split the string 106 | # around any 3 letter code in square brackets 107 | dep_station = re.split("\[[A-Z]{3}\]", txt(train, "from")) 108 | # The first bit is the station name 109 | t["from"] = dep_station[0].strip() 110 | # Now look for a platform 111 | try: 112 | platform = re.sub("\s\s+", " ", dep_station[1]) 113 | # IndexError will be raised if there is not dep_station[1] 114 | except IndexError: 115 | platform = "" 116 | finally: 117 | t["from_platform"] = platform 118 | 119 | # Get details of destination station 120 | # See notes above 121 | arr_station = re.split("\[[A-Z]{3}\]", txt(train, "to")) 122 | t["to"] = arr_station[0].strip() 123 | try: 124 | platform = re.sub("\s\s+", " ", arr_station[1]) 125 | except IndexError: 126 | platform = "" 127 | finally: 128 | t["to_platform"] = platform 129 | 130 | # Get remaining information 131 | # These should be self explanatory now. 132 | t["departing"] = txt(train, "dep") 133 | t["arriving"] = txt(train, "arr") 134 | t["duration"] = txt(train, "dur") 135 | t["changes"] = txt(train, "chg") 136 | t["status"] = status(train) 137 | 138 | # Add the train to our list 139 | trains.append(t) 140 | 141 | return trains 142 | 143 | 144 | def __hhmm(): 145 | # The site needs takes a time string in the format HHMM. 146 | # This function returns the current time in that format 147 | return datetime.now().strftime("%H%M") 148 | 149 | 150 | # These are our main functions i.e. the methods that a user may call 151 | # It's good practice to include a docstring to provide useful information as 152 | # to how to use the function and what to expect back. 153 | 154 | 155 | def lookup(start, dest, dep_time=None, dep_day=None, arriving=False): 156 | """Timetable lookup. Returns list of trains. 157 | 158 | Params: 159 | start: 3 letter code of departure station 160 | dest: 3 letter code of target station 161 | dep_time: HHMM formatted time. Leave blank for now. 162 | dep_day: DDMMYY formatted date. Leave blank for today. 163 | arriving: boolean. Set to True to search by arrival time. 164 | 165 | Each train object is a dictionary with the following keys: 166 | from: Name of starting station 167 | from_platform: Platform at starting station 168 | to: Destination station 169 | to_platform: Platform at destination station 170 | departing: Scheduled departure time 171 | arriving: Scheduled arrival time 172 | duration: Journey duration 173 | changes: Number of changes required 174 | status: Status of train 175 | """ 176 | # For "Now" searches we need current time... 177 | if not dep_time: 178 | dep_time = __hhmm() 179 | # ...and date as "today" 180 | if not dep_day: 181 | dep_day = "today" 182 | 183 | # This is the flag for whether we searching by departure or arrival time 184 | direction = "ARR" if arriving else "DEP" 185 | 186 | # build the URL with all the relevant bits 187 | suffix = SUFFIX.format(start=start, dest=dest, dep_day=dep_day, 188 | dep_time=dep_time, direction=direction) 189 | 190 | url = "{base}{suffix}".format(base=NR_BASE, suffix=suffix) 191 | 192 | # Get the full web page and send it to the parser 193 | trains = __parsePage(__getPage(url)) 194 | 195 | # If something's gone wrong, return an empty list, otherwise return the 196 | # details of the trains 197 | return trains if trains else [] 198 | 199 | 200 | def departures(start, dest=None): 201 | """Gets live departures. Returns list of trains. 202 | 203 | Params: 204 | start: Starting station 205 | dest: (Optional) Destination station. 206 | """ 207 | raise NotImplementedError 208 | 209 | 210 | def arrivals(dest, start=None): 211 | """Gets live arrivals. Returns list of trains. 212 | 213 | Params: 214 | dest: Destination station. 215 | start: (Optional) Starting station 216 | """ 217 | raise NotImplementedErrors 218 | -------------------------------------------------------------------------------- /screens/trains/screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | from kivy.clock import Clock 6 | from kivy.uix.label import Label 7 | from kivy.uix.screenmanager import Screen 8 | from kivy.uix.boxlayout import BoxLayout 9 | from kivy.uix.stacklayout import StackLayout 10 | from kivy.properties import StringProperty, ListProperty 11 | 12 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 13 | 14 | import nationalrail as NR 15 | 16 | 17 | class TrainJourney(Screen): 18 | desc = StringProperty("") 19 | headers = {"departing": "Dep.", 20 | "arriving": "Arr.", 21 | "changes": "Changes", 22 | "duration": "Dur.", 23 | "status": "Status", 24 | "from_platform": "Platform" 25 | } 26 | 27 | def __init__(self, **kwargs): 28 | super(TrainJourney, self).__init__(**kwargs) 29 | j = kwargs["journey"] 30 | self.desc = j["description"] 31 | self.to = j["to"] 32 | self.frm = j["from"] 33 | self.running = False 34 | self.nextupdate = 0 35 | self.timer = None 36 | 37 | def on_enter(self): 38 | # Calculate when the next update is due. 39 | if (time.time() > self.nextupdate): 40 | dt = 0.5 41 | else: 42 | dt = self.nextupdate - time.time() 43 | 44 | self.timer = Clock.schedule_once(self.getTrains, dt) 45 | 46 | def on_leave(self): 47 | Clock.unschedule(self.timer) 48 | 49 | def getTrains(self, *args): 50 | # Try loading the train data but handle any failure gracefully. 51 | try: 52 | trains = NR.lookup(self.frm, self.to) 53 | except: 54 | trains = None 55 | 56 | # If we've got trains then we need to set up the screen 57 | if trains: 58 | # Get rid of the previous widgets. 59 | self.clear_widgets() 60 | 61 | # Add a box layout 62 | self.bx = BoxLayout(orientation="vertical") 63 | 64 | # Show the name of the train route 65 | self.bx.add_widget(Label(text=self.desc, size_hint_y=0.2)) 66 | 67 | # Add headers for the trains 68 | self.bx.add_widget(TrainDetail(train=self.headers, 69 | bg=[0.2, 0.2, 0.2, 1])) 70 | 71 | # Create a StackLayout in case we need to scroll over the trains. 72 | self.stck = StackLayout(orientation="tb-lr", size_hint_y=0.8) 73 | self.bx.add_widget(self.stck) 74 | 75 | # Loop over the trains 76 | for train in trains: 77 | 78 | # Create a TrainDetail widget and add it to the StackLayout 79 | trn = TrainDetail(train=train) 80 | self.stck.add_widget(trn) 81 | 82 | # Get rid of the Loading label (if it's there) 83 | try: 84 | self.remove_widget(self.ids.load_label) 85 | except ReferenceError: 86 | pass 87 | 88 | self.add_widget(self.bx) 89 | 90 | # Set the next update for 5 mins later 91 | self.nextupdate = time.time() + 300 92 | self.timer = Clock.schedule_once(self.getTrains, 300) 93 | 94 | # No trains so let the user know. 95 | else: 96 | self.clear_widgets() 97 | errorm = ("Error getting train data.\nPlease check that you are " 98 | "connected to the internet and\nthat you have entered " 99 | "valid station names.") 100 | lb = Label(text=errorm) 101 | self.add_widget(lb) 102 | 103 | 104 | class TrainDetail(BoxLayout): 105 | """Custom widget to show detail for a specific train.""" 106 | departing = StringProperty("") 107 | arriving = StringProperty("") 108 | changes = StringProperty("") 109 | status = StringProperty("") 110 | duration = StringProperty("") 111 | platform = StringProperty("") 112 | bg = ListProperty([0.1, 0.1, 0.1, 1]) 113 | 114 | def __init__(self, **kwargs): 115 | super(TrainDetail, self).__init__(**kwargs) 116 | t = kwargs["train"] 117 | self.departing = t["departing"] 118 | self.arriving = t["arriving"] 119 | self.changes = t["changes"] 120 | self.duration = t["duration"] 121 | self.status = t["status"] 122 | self.platform = t.get("from_platform", "") 123 | self.bg = kwargs.get("bg", [0.1, 0.1, 0.1, 1]) 124 | 125 | 126 | class TrainScreen(Screen): 127 | def __init__(self, **kwargs): 128 | super(TrainScreen, self).__init__(**kwargs) 129 | self.params = kwargs["params"] 130 | self.journeys = self.params["journeys"] 131 | self.flt = self.ids.train_float 132 | self.flt.remove_widget(self.ids.train_base_box) 133 | self.scrmgr = self.ids.train_scrmgr 134 | self.running = False 135 | self.scrid = 0 136 | self.myscreens = ["{to}{from}".format(**x) for x in self.journeys] 137 | 138 | def on_enter(self): 139 | # If the screen hasn't been run before then we need to set up the 140 | # screens for the necessary train journeys. 141 | if not self.running: 142 | for journey in self.journeys: 143 | nm = "{to}{from}".format(**journey) 144 | self.scrmgr.add_widget(TrainJourney(journey=journey, name=nm)) 145 | self.running = True 146 | 147 | else: 148 | # Fixes bug where nested screens don't have "on_enter" or 149 | # "on_leave" methods called. 150 | for c in self.scrmgr.children: 151 | if c.name == self.scrmgr.current: 152 | c.on_enter() 153 | 154 | def on_leave(self): 155 | # Fixes bug where nested screens don't have "on_enter" or 156 | # "on_leave" methods called. 157 | for c in self.scrmgr.children: 158 | if c.name == self.scrmgr.current: 159 | c.on_leave() 160 | 161 | def next_screen(self, rev=True): 162 | a = self.myscreens 163 | n = -1 if rev else 1 164 | self.scrid = (self.scrid + n) % len(a) 165 | self.scrmgr.transition.direction = "up" if rev else "down" 166 | self.scrmgr.current = a[self.scrid] 167 | -------------------------------------------------------------------------------- /screens/trains/trains.kv: -------------------------------------------------------------------------------- 1 | 2 | FloatLayout: 3 | id: train_float 4 | 5 | BoxLayout: 6 | id: train_base_box 7 | Label: 8 | id: football_lbl_load 9 | text: "Loading journeys..." 10 | 11 | ScreenManager: 12 | id: train_scrmgr 13 | auto_bring_to_front: False 14 | 15 | HiddenButton: 16 | size: root.width, 50 17 | size_hint: None, None 18 | pos: 0, root.height - 50 19 | on_press: root.next_screen() 20 | 21 | HiddenButton: 22 | size: root.width, 50 23 | size_hint: None, None 24 | pos: 0, 0 25 | on_press: root.next_screen(False) 26 | 27 | 28 | Label: 29 | id: load_label 30 | text: root.desc 31 | 32 | 33 | orientation: "horizontal" 34 | size_hint: 1, None 35 | height: 50 36 | spacing: 5 37 | padding: 2 38 | 39 | BGLabel: 40 | text: root.departing 41 | bgcolour: root.bg 42 | 43 | BGLabel: 44 | text: root.arriving 45 | bgcolour: root.bg 46 | 47 | BGLabel: 48 | text: root.duration 49 | bgcolour: root.bg 50 | 51 | BGLabel: 52 | text: root.changes 53 | bgcolour: root.bg 54 | 55 | BGLabel: 56 | text: root.status 57 | bgcolour: root.bg 58 | size_hint_x: None 59 | width: 300 60 | 61 | BGLabel: 62 | text: root.platform 63 | bgcolour: root.bg 64 | -------------------------------------------------------------------------------- /screens/tube/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "TubeScreen", 3 | "kv": "tube.kv", 4 | "enabled": false, 5 | "dependencies": ["requests"], 6 | "params": {"colours": 7 | { 8 | "Bakerloo": 9 | { 10 | "text": "#FFFFFF", 11 | "background": "#AE6118" 12 | }, 13 | "Central": 14 | { 15 | "text": "#FFFFFF", 16 | "background": "#E41F1F" 17 | }, 18 | "Circle": 19 | { 20 | "text": "#113892", 21 | "background": "#F8D42D" 22 | }, 23 | "District": 24 | { 25 | "text": "#FFFFFF", 26 | "background": "#007229" 27 | }, 28 | "Hammersmith and City": 29 | { 30 | "text": "#113892", 31 | "background": "#E899A8" 32 | }, 33 | "Jubilee": 34 | { 35 | "text": "#FFFFFF", 36 | "background": "#686E72" 37 | }, 38 | "Metropolitan": 39 | { 40 | "text": "#FFFFFF", 41 | "background": "#893267" 42 | }, 43 | "Northern": 44 | { 45 | "text": "#FFFFFF", 46 | "background": "#000000" 47 | }, 48 | "Piccadilly": 49 | { 50 | "text": "#FFFFFF", 51 | "background": "#0450A1" 52 | }, 53 | "Victoria": 54 | { 55 | "text": "#FFFFFF", 56 | "background": "#009FE0" 57 | }, 58 | "Waterloo and City": 59 | { 60 | "text": "#113892", 61 | "background": "#70C3CE" 62 | }, 63 | "DLR": 64 | { 65 | "text": "#FFFFFF", 66 | "background": "#00BBB4" 67 | }, 68 | "Overground": 69 | { 70 | "text": "#FFFFFF", 71 | "background": "#FF8866" 72 | }, 73 | "TfL Rail": 74 | { 75 | "text": "#FFFFFF", 76 | "background": "#0450A1" 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /screens/tube/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/tube/resources/__init__.py -------------------------------------------------------------------------------- /screens/tube/resources/londonunderground.py: -------------------------------------------------------------------------------- 1 | """This script demonstrates how to use etreet to parse XMl information 2 | from a website and turn it into a structure that can then be used by other 3 | python codes. 4 | 5 | Please note, if you use TfL data in your application you should 6 | acknowledge the source of your data as appropriate. 7 | """ 8 | 9 | # Simple way of submitting web requests (easier to use than urllib2) 10 | import requests 11 | 12 | # we'll use the basic eTree parser to parse the XML file 13 | import xml.etree.cElementTree as et 14 | 15 | # We need regular expressions to strip part of the XML file out 16 | import re 17 | 18 | # Web address for tube data 19 | BASE_URL = "http://cloud.tfl.gov.uk/TrackerNet/LineStatus" 20 | 21 | 22 | def __getTubeData(): 23 | r = requests.get(BASE_URL) 24 | if r.status_code == 200: 25 | return r.content 26 | else: 27 | return None 28 | 29 | 30 | def TubeStatus(filterlines=None): 31 | """Parse the current status of London Underground lines. 32 | 33 | Takes one optional parameter: 34 | 35 | filterlines: List of tube lines to include in results. 36 | 37 | Returns a list of dict objects. Each dict represents an underground line: 38 | 39 | name: Name of line 40 | status: Short description of line status 41 | detail: Extended description. Will be the same as "status" unless there 42 | is disruption at which point this will contain more detail. 43 | 44 | Where filterlines has been passed then the result will only contain those 45 | requested lines. 46 | """ 47 | filtered = True if filterlines else False 48 | 49 | # Get the raw XML data 50 | rawstatus = __getTubeData() 51 | 52 | if rawstatus: 53 | # Strip out the xmlns tag (it just makes parsing the XML file more 54 | # difficult) 55 | rawstatus = re.sub(' xmlns="[^"]+"', '', rawstatus, count=1) 56 | # Loa it into eTree 57 | status = et.XML(rawstatus) 58 | else: 59 | return None 60 | 61 | # Create our empty list 62 | lines = [] 63 | 64 | # Loop over the lines 65 | for line in status.getchildren(): 66 | 67 | # Create an empty dict object for the information 68 | l = {} 69 | 70 | # Get the line name 71 | l["name"] = line.find("Line").get("Name") 72 | 73 | # Get the short description of the status 74 | l["status"] = line.find("Status").get("Description") 75 | 76 | # Get the extended definition 77 | detail = line.get("StatusDetails") 78 | l["detail"] = detail if detail else l["status"] 79 | 80 | # Add to the list 81 | lines.append(l) 82 | 83 | # Do we want to filter the results? 84 | if filtered: 85 | result = [x for x in lines if x["name"] in filterlines] 86 | else: 87 | result = lines 88 | 89 | return result 90 | -------------------------------------------------------------------------------- /screens/tube/screen.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from datetime import datetime 4 | import time 5 | 6 | from kivy.clock import Clock 7 | from kivy.core.window import Window 8 | from kivy.properties import (ObjectProperty, 9 | DictProperty, 10 | StringProperty, 11 | ListProperty) 12 | from kivy.uix.screenmanager import Screen 13 | from kivy.uix.boxlayout import BoxLayout 14 | 15 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | from resources.londonunderground import TubeStatus 18 | 19 | LINES = ['BAK', 20 | 'CEN', 21 | 'CIR', 22 | 'DIS', 23 | 'DLR', 24 | 'HAM', 25 | 'JUB', 26 | 'MET', 27 | 'NOR', 28 | 'OVE', 29 | 'PIC', 30 | 'TFL', 31 | 'VIC', 32 | 'WAT'] 33 | 34 | 35 | class TubeScreen(Screen): 36 | tube = DictProperty({}) 37 | 38 | def __init__(self, **kwargs): 39 | self.params = kwargs["params"] 40 | _NUMERALS = '0123456789abcdefABCDEF' 41 | self._HEXDEC = {v: int(v, 16) for v in (x+y for x in _NUMERALS 42 | for y in _NUMERALS)} 43 | self.build_dict() 44 | super(TubeScreen, self).__init__(**kwargs) 45 | self.timer = None 46 | 47 | def hex_to_kcol(self, hexcol): 48 | """Method to turn hex colour code to Kivy compatible list.""" 49 | if hexcol.startswith("#"): 50 | hexcol = hexcol[1:7] 51 | 52 | return [self._HEXDEC[hexcol[0:2]]/255., 53 | self._HEXDEC[hexcol[2:4]]/255., 54 | self._HEXDEC[hexcol[4:6]]/255., 55 | 1] 56 | 57 | def build_dict(self): 58 | """Creates default entries in dictionary of tube status.""" 59 | for l in LINES: 60 | # Alert user that we're waiting for data. 61 | self.tube[l] = "Loading data..." 62 | 63 | # Get the colours and create a dictionary 64 | coldict = self.params["colours"] 65 | self.coldict = {x[:3].upper(): coldict[x] for x in coldict} 66 | 67 | # Convert Tube hex colours to Kivy colours. 68 | hk = self.hex_to_kcol 69 | for c in self.coldict: 70 | self.coldict[c]["background"] = hk(self.coldict[c]["background"]) 71 | self.coldict[c]["text"] = hk(self.coldict[c]["text"]) 72 | self.tube["colours"] = self.coldict 73 | 74 | self.tube["update"] = "Waiting for data..." 75 | 76 | def update(self, dt): 77 | # Get the tube data or handle failure to retrieve data. 78 | try: 79 | raw = TubeStatus() 80 | except: 81 | raw = None 82 | 83 | # If we've got data, let's show the status 84 | if raw: 85 | temp = {x["name"][:3].upper(): x["status"] for x in raw} 86 | 87 | # Otherwise just say there's been an error. 88 | else: 89 | temp = {x: "Error." for x in LINES} 90 | 91 | for k in temp: 92 | self.tube[k] = temp[k] 93 | 94 | # Add the additional detail 95 | if raw: 96 | self.tube["detail"] = {x["name"][:3].upper(): x["detail"] 97 | for x in raw} 98 | self.tube["name"] = {x["name"][:3].upper(): x["name"] 99 | for x in raw} 100 | else: 101 | self.tube["detail"] = {x: "Error retrieving data." for x in LINES} 102 | self.tube["name"] = {x: x for x in LINES} 103 | 104 | self.tube["colours"] = self.coldict 105 | 106 | if raw: 107 | updt = datetime.now().strftime("%H:%M") 108 | self.tube["update"] = "Last updated at {}".format(updt) 109 | self.nextupdate = time.time() + 300 110 | 111 | def on_enter(self): 112 | self.update(None) 113 | self.timer = Clock.schedule_interval(self.update, 5 * 60) 114 | 115 | def on_leave(self): 116 | Clock.unschedule(self.timer) 117 | 118 | def show_info(self, line): 119 | """If user clicks on tube line we need to show extra data. 120 | 121 | This method creates a TubeDetail widget instance and displays it. 122 | """ 123 | w = TubeDetail(line=self.tube["name"][line], 124 | detail=self.tube["detail"][line], 125 | bg=self.tube["colours"][line]["background"], 126 | fg=self.tube["colours"][line]["text"]) 127 | self.ids.tubefloat.add_widget(w) 128 | 129 | 130 | class TubeDetail(BoxLayout): 131 | line = StringProperty("") 132 | detail = StringProperty("") 133 | bg = ListProperty([]) 134 | fg = ListProperty([]) 135 | 136 | def _init__(self, **kwargs): 137 | super(TubeDetail, self).__init__(**kwargs) 138 | self.line = kwargs["line"] 139 | self.detail = kwargs["detail"] 140 | self.bg = kwargs["bg"] 141 | self.fg = kwargs["fg"] 142 | -------------------------------------------------------------------------------- /screens/weather/README: -------------------------------------------------------------------------------- 1 | Weather forecast 2 | ---------------- 3 | 4 | API Key: 5 | 6 | This plugin requires you to register for an API ley from weatherunderground. 7 | 8 | See: http://www.wunderground.com/weather/api 9 | 10 | As the screen displays hourly data, you'll need a "Cumulus" or "Anvill" plan key. These should be free if you keep your requests low (and for this reason the screen should only poll once every hour). 11 | 12 | 13 | Configuring locations: 14 | 15 | There's a number of ways to set up locations. Have a look at the "query" section on this page: 16 | 17 | http://www.wunderground.com/weather/api/d/docs?d=data/index 18 | 19 | Any of those formats can be entered into the "address" part of the config file. 20 | 21 | 22 | Known issues: 23 | ------------- 24 | 25 | - Images flashing 26 | - Layout is a bit rubbish! 27 | -------------------------------------------------------------------------------- /screens/weather/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "WeatherScreen", 3 | "kv": "weather.kv", 4 | "enabled": false, 5 | "dependencies": ["requests"], 6 | "params": { 7 | "key": "YOUR_API_KEY_HERE", 8 | "locations": [ 9 | {"name": "London", 10 | "address": "UK/London.json"}, 11 | {"name": "Current location", 12 | "address": "autoip.json"} 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /screens/weather/screen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import requests 4 | import time 5 | 6 | from kivy.uix.label import Label 7 | from kivy.uix.screenmanager import Screen 8 | from kivy.uix.boxlayout import BoxLayout 9 | from kivy.properties import StringProperty 10 | from kivy.clock import Clock 11 | from kivy.uix.scrollview import ScrollView 12 | 13 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 14 | 15 | 16 | class WeatherForecastHourly(BoxLayout): 17 | """Custom widget to show hourly forecast summary.""" 18 | weather = StringProperty("") 19 | 20 | def __init__(self, **kwargs): 21 | super(WeatherForecastHourly, self).__init__(**kwargs) 22 | self.buildText(kwargs["summary"]) 23 | 24 | def buildText(self, summary): 25 | fc = {} 26 | tm = summary["FCTTIME"] 27 | fc["dy"] = "{} {}{}".format(tm["weekday_name_abbrev"], 28 | tm["hour"], 29 | tm["ampm"].lower()) 30 | fc["su"] = summary["condition"] 31 | fc["hg"] = summary["temp"]["metric"] 32 | fc["po"] = summary["pop"] 33 | self.weather = ("{dy}\n{su}\nHigh: " 34 | "{hg}{dg}\nRain: {po}%").format(dg="C", **fc) 35 | 36 | 37 | class WeatherForecastDay(BoxLayout): 38 | """Custom widget to show daily forecast summary.""" 39 | weather = StringProperty("") 40 | icon_url = StringProperty("") 41 | day = StringProperty("") 42 | 43 | def __init__(self, **kwargs): 44 | super(WeatherForecastDay, self).__init__(**kwargs) 45 | self.buildText(kwargs["summary"]) 46 | 47 | def buildText(self, summary): 48 | fc = {} 49 | self.day = summary["date"]["weekday_short"] 50 | fc["su"] = summary["conditions"] 51 | fc["hg"] = summary["high"]["celsius"] 52 | fc["lw"] = summary["low"]["celsius"] 53 | fc["po"] = summary["pop"] 54 | self.icon_url = summary["icon_url"] 55 | self.weather = ("{su}\nHigh: {hg}{dg}\n" 56 | "Low: {lw}\nRain: {po}%").format(dg="C", **fc) 57 | 58 | 59 | class WeatherSummary(Screen): 60 | """Screen to show weather summary for a selected location.""" 61 | location = StringProperty("") 62 | 63 | def __init__(self, **kwargs): 64 | super(WeatherSummary, self).__init__(**kwargs) 65 | self.location = kwargs["location"] 66 | self.url_forecast = kwargs["forecast"] 67 | self.url_hourly = kwargs["hourly"] 68 | self.bx_forecast = self.ids.bx_forecast 69 | self.bx_hourly = self.ids.bx_hourly 70 | self.nextupdate = 0 71 | self.timer = None 72 | 73 | def on_enter(self): 74 | # Check if the next update is due 75 | if (time.time() > self.nextupdate): 76 | dt = 0.5 77 | else: 78 | dt = self.nextupdate - time.time() 79 | 80 | self.timer = Clock.schedule_once(self.getData, dt) 81 | 82 | def on_leave(self): 83 | Clock.unschedule(self.timer) 84 | 85 | def getData(self, *args): 86 | # Try to get the daily data but handle any failure to do so. 87 | try: 88 | self.forecast = requests.get(self.url_forecast).json() 89 | days = self.forecast["forecast"]["simpleforecast"]["forecastday"] 90 | except: 91 | days = None 92 | 93 | # Try to get the hourly data but handle any failure to do so. 94 | try: 95 | self.hourly = requests.get(self.url_hourly).json() 96 | hours = self.hourly["hourly_forecast"] 97 | except: 98 | hours = None 99 | 100 | # Clear the screen of existing widgets 101 | self.bx_forecast.clear_widgets() 102 | self.bx_hourly.clear_widgets() 103 | 104 | # If we've got daily info then we can display it. 105 | if days: 106 | for day in days: 107 | frc = WeatherForecastDay(summary=day) 108 | self.bx_forecast.add_widget(frc) 109 | 110 | # If not, let the user know. 111 | else: 112 | lb_error = Label(text="Error getting weather data.") 113 | self.bx_forecast.add_widget(lb_error) 114 | 115 | # If we've got hourly weather data then show it 116 | if hours: 117 | 118 | # We need a scroll view as there's a lot of data... 119 | w = len(hours) * 45 120 | bx = BoxLayout(orientation="horizontal", size=(w, 180), 121 | size_hint=(None, None), spacing=5) 122 | sv = ScrollView(size_hint=(1, 1)) 123 | sv.add_widget(bx) 124 | 125 | for hour in hours: 126 | frc = WeatherForecastHourly(summary=hour) 127 | bx.add_widget(frc) 128 | self.bx_hourly.add_widget(sv) 129 | 130 | # If there's no data, let the user know 131 | else: 132 | lb_error = Label(text="Error getting weather data.") 133 | self.bx_forecast.add_widget(lb_error) 134 | 135 | # We're done, so schedule the next update 136 | if hours and days: 137 | dt = 60 * 60 138 | else: 139 | dt = 5 * 60 140 | 141 | self.nextupdate = time.time() + dt 142 | self.timer = Clock.schedule_once(self.getData, dt) 143 | 144 | 145 | class WeatherScreen(Screen): 146 | forecast = "http://api.wunderground.com/api/{key}/forecast/q/{location}" 147 | hourly = "http://api.wunderground.com/api/{key}/hourly/q/{location}" 148 | 149 | def __init__(self, **kwargs): 150 | super(WeatherScreen, self).__init__(**kwargs) 151 | self.key = kwargs["params"]["key"] 152 | self.locations = kwargs["params"]["locations"] 153 | self.flt = self.ids.weather_float 154 | self.flt.remove_widget(self.ids.weather_base_box) 155 | self.scrmgr = self.ids.weather_scrmgr 156 | self.running = False 157 | self.scrid = 0 158 | self.myscreens = [x["address"] for x in self.locations] 159 | 160 | def on_enter(self): 161 | # If the screen hasn't been displayed before then let's load up 162 | # the locations 163 | if not self.running: 164 | for location in self.locations: 165 | 166 | # Create the necessary URLs for the data 167 | forecast, hourly = self.buildURLs(location["address"]) 168 | 169 | # Create a weather summary screen 170 | ws = WeatherSummary(forecast=forecast, 171 | hourly=hourly, 172 | name=location["address"], 173 | location=location["name"]) 174 | 175 | # and add to our screen manager. 176 | self.scrmgr.add_widget(ws) 177 | 178 | # set the flag so we don't do this again. 179 | self.running = True 180 | 181 | else: 182 | # Fixes bug where nested screens don't have "on_enter" or 183 | # "on_leave" methods called. 184 | for c in self.scrmgr.children: 185 | if c.name == self.scrmgr.current: 186 | c.on_enter() 187 | 188 | def on_leave(self): 189 | # Fixes bug where nested screens don't have "on_enter" or 190 | # "on_leave" methods called. 191 | for c in self.scrmgr.children: 192 | if c.name == self.scrmgr.current: 193 | c.on_leave() 194 | 195 | def buildURLs(self, location): 196 | return (self.forecast.format(key=self.key, location=location), 197 | self.hourly.format(key=self.key, location=location)) 198 | 199 | def next_screen(self, rev=True): 200 | a = self.myscreens 201 | n = -1 if rev else 1 202 | self.scrid = (self.scrid + n) % len(a) 203 | self.scrmgr.transition.direction = "up" if rev else "down" 204 | self.scrmgr.current = a[self.scrid] 205 | -------------------------------------------------------------------------------- /screens/weather/weather.kv: -------------------------------------------------------------------------------- 1 | 2 | FloatLayout: 3 | id: weather_float 4 | 5 | BoxLayout: 6 | id: weather_base_box 7 | Label: 8 | id: weather_lbl_load 9 | text: "Loading weather..." 10 | 11 | ScreenManager: 12 | id: weather_scrmgr 13 | auto_bring_to_front: False 14 | 15 | HiddenButton: 16 | size: root.width, 50 17 | size_hint: None, None 18 | pos: 0, root.height - 50 19 | on_press: root.next_screen() 20 | 21 | HiddenButton: 22 | size: root.width, 50 23 | size_hint: None, None 24 | pos: 0, 0 25 | on_press: root.next_screen(False) 26 | 27 | 28 | BoxLayout: 29 | padding: 5, 5 30 | orientation: "vertical" 31 | Label: 32 | text: root.location 33 | size_hint_y: 0.2 34 | 35 | BoxLayout: 36 | padding: 2 37 | id: bx_forecast 38 | orientation: "horizontal" 39 | size_hint_y: 0.4 40 | spacing: 5 41 | 42 | BoxLayout: 43 | padding: 2 44 | id: bx_hourly 45 | orientation: "horizontal" 46 | size_hint_y: 0.4 47 | spacing: 5 48 | 49 | 50 | orientation: "vertical" 51 | canvas.before: 52 | Color: 53 | rgba: 0.1, 0.1, 0.1, 1 54 | Rectangle: 55 | pos: self.pos 56 | size: self.size 57 | BoxLayout: 58 | size_hint_y: 0.4 59 | orientation: "horizontal" 60 | 61 | Label: 62 | text: root.day 63 | font_size: 18 64 | 65 | AsyncImage: 66 | source: root.icon_url 67 | Label: 68 | text: root.weather 69 | 70 | 71 | width: 100 72 | size_hint_x: None 73 | orientation: "vertical" 74 | canvas.before: 75 | Color: 76 | rgba: 0.1, 0.1, 0.1, 1 77 | Rectangle: 78 | pos: self.pos 79 | size: self.size 80 | 81 | Label: 82 | text: root.weather 83 | -------------------------------------------------------------------------------- /screens/wordclock/README: -------------------------------------------------------------------------------- 1 | Wordclock 2 | --------- 3 | 4 | Simple wordclock screen which is the default screen for this project. 5 | 6 | There are two available parameters: 7 | - "language": selects layout for various languages (default: english) 8 | - "colour": RGB colour code for displaying the time 9 | 10 | Languages 11 | --------- 12 | 13 | Currently the following languages are available: 14 | - English 15 | - Spanish 16 | - Portuguese 17 | - French 18 | - Swedish 19 | - Finnish 20 | 21 | Anybody is welcome to submit further languages! 22 | -------------------------------------------------------------------------------- /screens/wordclock/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "WordClockScreen", 3 | "kv": "wordclock.kv", 4 | "dependencies": [], 5 | "enabled": true, 6 | "params": { 7 | "language": "english", 8 | "colour": [0, 200, 200] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elParaguayo/RPi-InfoScreen-Kivy/2b3612c4e577add0f5563fe279e555e2c153fac3/screens/wordclock/layouts/__init__.py -------------------------------------------------------------------------------- /screens/wordclock/layouts/dutch.py: -------------------------------------------------------------------------------- 1 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 2 | 3 | Custom layouts can be created for the screen by creating a new file in the 4 | "layouts" folder. 5 | 6 | Each layout must have the following variables: 7 | LAYOUT: The grid layout. Must be a single string. 8 | MAP: The mapping required for various times (see notes below) 9 | COLS: The number of columns required for the grid layout 10 | SIZE: The size of the individual box containing your letter. 11 | Tuple in (x, y) format. 12 | FONTSIZE: Font size for the letter 13 | ''' 14 | 15 | # Layout is a single string variable which will be looped over by the parser. 16 | LAYOUT = ("HETGISBNUL" 17 | "DVIJFTIENV" 18 | "KWARTXOVER" 19 | "VOORXHALFJ" 20 | "ACHTWEEZES" 21 | "DRIELFTIEN" 22 | "CZEVENEGEN" 23 | "VIERTWAALF" 24 | "EENVIJFUUR") 25 | 26 | # Map instructions: 27 | # The clock works by rounding the time to the nearest 5 minutes. 28 | # This means that you need to have settngs for each five minute interval "m00" 29 | # "m00", "m05". 30 | # The clock also works on a 12 hour basis rather than 24 hour: 31 | # "h00", "h01" etc. 32 | # There are three optional parameters: 33 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 34 | # "am": Wording/symbol to indicate morning. 35 | # "pm": Wording/symbol to indicate afternoon/evening 36 | MAP = { 37 | "all": [0, 1, 2, 4, 5], # HET IS 38 | "m00": [87,88,89], # UUR 39 | "m05": [11, 12, 13, 14, 26, 27, 28, 29], #VIJF OVER 40 | "m10": [15, 16, 17, 18, 26, 27, 28, 29], #TIEN OVER 41 | "m15": [20, 21, 22, 23, 24, 26, 27, 28, 29], #KWART OVER 42 | "m20": [15, 16, 17, 18, 30, 31 ,32 ,33, 35, 36, 37, 38], #TIEN VOOR HALF 43 | "m25": [11, 12, 13, 14, 30, 31 ,32 ,33, 35, 36, 37, 38], #VIJF VOOR HALF 44 | "m30": [35, 36, 37, 38], #HALF 45 | "m35": [11, 12, 13, 14, 26, 27, 28, 29, 35, 36, 37, 38], #VIJF OVER HALF 46 | "m40": [15, 16, 17, 18, 26, 27, 28, 29, 35, 36, 37, 38], #TIEN OVER HALF 47 | "m45": [20, 21, 22, 23, 24, 30, 31 ,32 ,33], #KWART VOOR 48 | "m50": [15, 16, 17, 18, 30, 31 ,32 ,33], #TIEN VOOR 49 | "m55": [11, 12, 13, 14, 30, 31 ,32 ,33], #VIJF VOOR 50 | "h01": [80, 81, 82], #EEN (D) 51 | "h02": [43, 44, 45, 46], #TWEE (D) 52 | "h03": [50, 51, 52, 53], #DRIE (D) 53 | "h04": [70, 71, 72, 73], #VIER (D) 54 | "h05": [83, 84, 85, 86], #VIJF (D) 55 | "h06": [47, 48, 49], #ZES (D) 56 | "h07": [61, 62, 63, 64, 65], #ZEVEN (D) 57 | "h08": [40, 41, 42, 43], #ACHT (D) 58 | "h09": [65, 66, 67, 68, 69], #NEGEN (D) 59 | "h10": [56, 57, 58, 59], #TIEN (D) 60 | "h11": [53, 54, 55], #ELF (D) 61 | "h12": [74, 75, 76, 77, 78, 79], #TWAALF (D) 62 | } 63 | 64 | # Number of columns in grid layout 65 | COLS = 10 66 | 67 | # Size of letter in grid (x, y) 68 | SIZE = (80, 53) 69 | 70 | # Font size of letter 71 | FONTSIZE = 50 72 | 73 | # Is our language one where we need to increment the hour after 30 mins 74 | # e.g. 9:40 is "Twenty to ten" 75 | HOUR_INCREMENT = True 76 | 77 | HOUR_INCREMENT_TIME = 15 78 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/english.py: -------------------------------------------------------------------------------- 1 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 2 | 3 | Custom layouts can be created for the screen by creating a new file in the 4 | "layouts" folder. 5 | 6 | Each layout must have the following variables: 7 | LAYOUT: The grid layout. Must be a single string. 8 | MAP: The mapping required for various times (see notes below) 9 | COLS: The number of columns required for the grid layout 10 | SIZE: The size of the individual box containing your letter. 11 | Tuple in (x, y) format. 12 | FONTSIZE: Font size for the letter 13 | ''' 14 | 15 | # Layout is a single string variable which will be looped over by the parser. 16 | LAYOUT = ("ITQISHCUBMWLRPI" 17 | "AOQUARTERFDHALF" 18 | "TWENTYSFIVEGTEN" 19 | "TOXPASTNYTWELVE" 20 | "ONESIXTHREENINE" 21 | "SEVENTWOXELEVEN" 22 | "EIGHTENFOURFIVE" 23 | "RPIO'CLOCKHAMPM") 24 | 25 | # Map instructions: 26 | # The clock works by rounding the time to the nearest 5 minutes. 27 | # This means that you need to have settngs for each five minute interval "m00" 28 | # "m00", "m05". 29 | # The clock also works on a 12 hour basis rather than 24 hour: 30 | # "h00", "h01" etc. 31 | # There are three optional parameters: 32 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 33 | # "am": Wording/symbol to indicate morning. 34 | # "pm": Wording/symbol to indicate afternoon/evening 35 | MAP = { 36 | "all": [0, 1, 3, 4], 37 | "m00": [108, 109, 110, 111, 112, 113, 114], 38 | "m05": [37,38, 39, 40, 48, 49, 50, 51], 39 | "m10": [42, 43, 44, 48, 49, 50, 51], 40 | "m15": [15, 17, 18, 19, 20, 21, 22, 23, 48, 49, 50, 51], 41 | "m20": [30, 31, 32, 33, 34, 35, 48, 49, 50, 51], 42 | "m25": [30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 48, 49, 50, 51], 43 | "m30": [26, 27, 28, 29, 48, 49, 50, 51], 44 | "m35": [30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 45, 46], 45 | "m40": [30, 31, 32, 33, 34, 35, 45, 46], 46 | "m45": [15, 17, 18, 19, 20, 21, 22, 23, 45, 46], 47 | "m50": [42, 43, 44, 45, 46], 48 | "m55": [37, 38, 39, 40, 45, 46], 49 | "h01": [60, 61, 62], 50 | "h02": [80, 81, 82], 51 | "h03": [66, 67, 68, 69, 70], 52 | "h04": [97, 98, 99, 100], 53 | "h05": [101, 102, 103, 104], 54 | "h06": [63, 64, 65], 55 | "h07": [75, 76, 77, 78, 79], 56 | "h08": [90, 91, 92, 93, 94], 57 | "h09": [71, 72, 73, 74], 58 | "h10": [94, 95, 96], 59 | "h11": [84, 85, 86, 87, 88, 89], 60 | "h12": [54, 55, 56, 57, 58, 59], 61 | "am": [116, 117], 62 | "pm": [118, 119] 63 | } 64 | 65 | # Number of columns in grid layout 66 | COLS = 15 67 | 68 | # Size of letter in grid (x, y) 69 | SIZE = (53, 60) 70 | 71 | # Font size of letter 72 | FONTSIZE = 40 73 | 74 | # Is our language one where we need to increment the hour after 30 mins 75 | # e.g. 9:40 is "Twenty to ten" 76 | HOUR_INCREMENT = True 77 | 78 | HOUR_INCREMENT_TIME = 30 79 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/finnish.py: -------------------------------------------------------------------------------- 1 | # This Python file uses the following encoding: utf-8 2 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 3 | 4 | Custom layouts can be created for the screen by creating a new file in the 5 | "layouts" folder. 6 | 7 | Each layout must have the following variables: 8 | LAYOUT: The grid layout. Must be a single string. 9 | MAP: The mapping required for various times (see notes below) 10 | COLS: The number of columns required for the grid layout 11 | SIZE: The size of the individual box containing your letter. 12 | Tuple in (x, y) format. 13 | FONTSIZE: Font size for the letter 14 | ''' 15 | 16 | # Layout is a single string variable which will be looped over by the parser. 17 | LAYOUT = (u"KELLOQONYPUOLIWPWKL" #0 18 | u"AKYMMENTÄXVARTTIAZC" #19 19 | u"KAHTAKYMMENTÄVIITTÄ" #38 20 | u"VAILLEYLIBSEITSEMÄN" #57 21 | u"YKSITOISTAKAHDEKSAN" #76 22 | u"KAKSITOISTAYHDEKSÄN" #95 23 | u"KYMMENENTKOLMENELJÄ" #114 24 | u"KUUSIVIISIIIVAAPIIP") #133 25 | 26 | # Map instructions: 27 | # The clock works by rounding the time to the nearest 5 minutes. 28 | # This means that you need to have settngs for each five minute interval "m00" 29 | # "m00", "m05". 30 | # The clock also works on a 12 hour basis rather than 24 hour: 31 | # "h00", "h01" etc. 32 | # There are three optional parameters: 33 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 34 | # "am": Wording/symbol to indicate morning. 35 | # "pm": Wording/symbol to indicate afternoon/evening 36 | MAP = { 37 | "all": [0, 1, 2, 3, 4, 6, 7], 38 | "m00": [], 39 | "m05": [51, 52, 53, 54, 55, 56, 63, 64, 65], 40 | "m10": [20, 21, 22, 23, 24, 25, 26, 27, 63, 64, 65], 41 | "m15": [29, 30, 31, 32, 33, 34, 35, 63, 64, 65], 42 | "m20": [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 63, 64, 65], 43 | "m25": [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 63, 64, 65], 44 | "m30": [9, 10, 11, 12, 13], 45 | "m35": [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62], 46 | "m40": [38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 57, 58, 59, 60, 61, 62], 47 | "m45": [29, 30, 31, 32, 33, 34, 35, 57, 58, 59, 60, 61, 62], 48 | "m50": [20, 21, 22, 23, 24, 25, 26, 27, 57, 58, 59, 60, 61, 62], 49 | "m55": [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62], 50 | "h01": [76, 77, 78, 79], 51 | "h02": [95, 96, 97, 98, 99], 52 | "h03": [123, 124, 125, 126, 127], 53 | "h04": [128, 129, 130, 131, 132], 54 | "h05": [138, 139, 140, 141, 142], 55 | "h06": [133, 134, 135, 136, 137], 56 | "h07": [67, 68, 69, 70, 71, 72, 73, 74, 75], 57 | "h08": [86, 87, 88, 89, 90, 91, 92, 93, 94], 58 | "h09": [106, 107, 108, 109, 110, 111, 112, 113], 59 | "h10": [114, 115, 116, 117, 118, 119, 120, 121], 60 | "h11": [76, 77, 78, 79, 80, 81, 82, 83, 84, 85], 61 | "h12": [95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105], 62 | "am": [147, 148], 63 | "pm": [150, 151] 64 | } 65 | 66 | # Number of columns in grid layout 67 | COLS = 19 68 | 69 | # Size of letter in grid (x, y) 70 | SIZE = (42, 60) 71 | 72 | # Font size of letter 73 | FONTSIZE = 40 74 | 75 | # Is our language one where we need to increment the hour after 30 mins 76 | # e.g. 9:40 is "Twenty to ten" 77 | HOUR_INCREMENT = True 78 | 79 | HOUR_INCREMENT_TIME = 29 80 | 81 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/french.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 3 | 4 | Custom layouts can be created for the screen by creating a new file in the 5 | "layouts" folder. 6 | 7 | Each layout must have the following variables: 8 | LAYOUT: The grid layout. Must be a single string. 9 | MAP: The mapping required for various times (see notes below) 10 | COLS: The number of columns required for the grid layout 11 | SIZE: The size of the individual box containing your letter. 12 | Tuple in (x, y) format. 13 | FONTSIZE: Font size for the letter 14 | ''' 15 | 16 | # Layout is a single string variable which will be looped over by the parser. 17 | LAYOUT = ("ILNESTHUNEDEUXP" 18 | "TROISIXOQUATREM" 19 | "CINQHUITNEUFDIX" 20 | "ONZEBDOUZERSEPT" 21 | "HEURESRASPBERRY" 22 | "ETMOINSUCINQDIX" 23 | "LEJQUARTFDEMIEL" 24 | "RPIVINGT-CINQKU" 25 | ) 26 | 27 | # Map instructions: 28 | # The clock works by rounding the time to the nearest 5 minutes. 29 | # This means that you need to have settngs for each five minute interval "m00" 30 | # "m00", "m05". 31 | # The clock also works on a 12 hour basis rather than 24 hour: 32 | # "h00", "h01" etc. 33 | # There are three optional parameters: 34 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 35 | # "am": Wording/symbol to indicate morning. 36 | # "pm": Wording/symbol to indicate afternoon/evening 37 | MAP = { 38 | "all": [0, 1, 3, 4, 5, 60, 61, 62, 63, 64], 39 | "m00": [], 40 | "m05": [83, 84, 85, 86], 41 | "m10": [87, 88, 89], 42 | "m15": [75, 76, 93, 94, 95, 96, 97], 43 | "m20": [108, 109, 110, 111, 112], 44 | "m25": [108, 109, 110, 111, 112, 113, 114, 115, 116, 117], 45 | "m30": [75, 76, 99, 100, 101, 102, 103], 46 | "m35": [77, 78, 79, 80, 81, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117], 47 | "m40": [77, 78, 79, 80, 81, 108, 109, 110, 111, 112], 48 | "m45": [77, 78, 79, 80, 81, 90, 91, 93, 94, 95, 96, 97], 49 | "m50": [77, 78, 79, 80, 81, 87, 88, 89], 50 | "m55": [77, 78, 79, 80, 81, 83, 84, 85, 86], 51 | "h01": [7, 8, 9], 52 | "h02": [65, 10, 11, 12, 13], 53 | "h03": [65, 15, 16, 17, 18], 54 | "h04": [65, 23, 24, 25, 26, 27, 28], 55 | "h05": [65, 30, 31, 32, 33], 56 | "h06": [65, 19, 20, 21], 57 | "h07": [65, 56, 57, 58, 59], 58 | "h08": [65, 34, 35, 36, 37], 59 | "h09": [65, 38, 39, 40, 41], 60 | "h10": [65, 42, 43, 44], 61 | "h11": [65, 45, 46, 47, 48], 62 | "h12": [65, 50, 51, 52, 53, 54], 63 | "am": [], 64 | "pm": [], 65 | } 66 | 67 | # Number of columns in grid layout 68 | COLS = 15 69 | 70 | # Size of letter in grid (x, y) 71 | SIZE = (53, 60) 72 | 73 | # Font size of letter 74 | FONTSIZE = 40 75 | 76 | # Is our language one where we need to increment the hour after 30 mins 77 | # e.g. 9:40 is "Twenty to ten" 78 | HOUR_INCREMENT = True 79 | 80 | HOUR_INCREMENT_TIME = 30 81 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/portuguese.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 3 | 4 | Custom layouts can be created for the screen by creating a new file in the 5 | "layouts" folder. 6 | 7 | Each layout must have the following variables: 8 | LAYOUT: The grid layout. Must be a single string. 9 | MAP: The mapping required for various times (see notes below) 10 | COLS: The number of columns required for the grid layout 11 | SIZE: The size of the individual box containing your letter. 12 | Tuple in (x, y) format. 13 | FONTSIZE: Font size for the letter 14 | ''' 15 | 16 | # Layout is a single string variable which will be looped over by the parser. 17 | LAYOUT = (u"ESÃORUMAPDUASNT" 18 | u"TRESQUATROCINCO" 19 | u"SEISGSETEVOITOM" 20 | u"NOVEDEZONZEDOZE" 21 | u"HORASOEÃMENOSLP" 22 | u"UMXQUARTOSRPDEZ" 23 | u"VINTEZEJCINCORX" 24 | u"ETMEIAEMUPONTOP" 25 | ) 26 | 27 | # Map instructions: 28 | # The clock works by rounding the time to the nearest 5 minutes. 29 | # This means that you need to have settngs for each five minute interval "m00" 30 | # "m00", "m05". 31 | # The clock also works on a 12 hour basis rather than 24 hour: 32 | # "h00", "h01" etc. 33 | # There are three optional parameters: 34 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 35 | # "am": Wording/symbol to indicate morning. 36 | # "pm": Wording/symbol to indicate afternoon/evening 37 | MAP = { 38 | "all": [60, 61, 62, 63], 39 | "m00": [111, 112, 114, 115, 116, 117, 118], 40 | "m05": [66, 98, 99, 100, 101, 102], 41 | "m10": [66, 87, 88, 89], 42 | "m15": [66, 75, 76, 78, 79, 80, 81, 82, 83], 43 | "m20": [66, 90, 91, 92, 93, 94], 44 | "m25": [66, 90, 91, 92, 93, 94, 96, 98, 99, 100, 101, 102], 45 | "m30": [66, 107, 108, 109, 110], 46 | "m35": [68, 69, 70, 71, 72, 90, 91, 92, 93, 94, 96, 98, 99, 100, 101, 102], 47 | "m40": [68, 69, 70, 71, 72, 90, 91, 92, 93, 94], 48 | "m45": [68, 69, 70, 71, 72, 75, 76, 78, 79, 80, 81, 82, 83], 49 | "m50": [68, 69, 70, 71, 72, 87, 88, 89], 50 | "m55": [68, 69, 70, 71, 72, 98, 99, 100, 101, 102], 51 | "h01": [0, 5, 6, 7], 52 | "h02": [1, 2, 3, 64, 9, 10, 11, 12], 53 | "h03": [1, 2, 3, 64, 15, 16, 17, 18], 54 | "h04": [1, 2, 3, 64, 19, 20, 21, 22, 23, 24], 55 | "h05": [1, 2, 3, 64, 25, 26, 27, 28, 29], 56 | "h06": [1, 2, 3, 64, 30, 31, 32, 33], 57 | "h07": [1, 2, 3, 64, 35, 36, 37, 38], 58 | "h08": [1, 2, 3, 64, 40, 41, 42, 43], 59 | "h09": [1, 2, 3, 64, 45, 46, 47, 48], 60 | "h10": [1, 2, 3, 64, 49, 50, 51], 61 | "h11": [1, 2, 3, 64, 52, 53, 54, 55], 62 | "h12": [1, 2, 3, 64, 56, 57, 58, 59], 63 | "am": [], 64 | "pm": [] 65 | } 66 | 67 | # Number of columns in grid layout 68 | COLS = 15 69 | 70 | # Size of letter in grid (x, y) 71 | SIZE = (53, 60) 72 | 73 | # Font size of letter 74 | FONTSIZE = 40 75 | 76 | # Is our language one where we need to increment the hour after 30 mins 77 | # e.g. 9:40 is "Twenty to ten" 78 | HOUR_INCREMENT = True 79 | 80 | HOUR_INCREMENT_TIME = 30 81 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/spanish.py: -------------------------------------------------------------------------------- 1 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 2 | 3 | Custom layouts can be created for the screen by creating a new file in the 4 | "layouts" folder. 5 | 6 | Each layout must have the following variables: 7 | LAYOUT: The grid layout. Must be a single string. 8 | MAP: The mapping required for various times (see notes below) 9 | COLS: The number of columns required for the grid layout 10 | SIZE: The size of the individual box containing your letter. 11 | Tuple in (x, y) format. 12 | FONTSIZE: Font size for the letter 13 | ''' 14 | 15 | # Layout is a single string variable which will be looped over by the parser. 16 | LAYOUT = ("ESONPLASWUNADOS" 17 | "TRESCUATROCINCO" 18 | "SEISIETEOCHONCE" 19 | "NUEVESDIEZVDOCE" 20 | "YMENOSQCINCORPI" 21 | "DIEZTRCUARTOELP" 22 | "VEINTEBMEDIALZI" 23 | "RPIVEINTICINCOR" 24 | ) 25 | 26 | # Map instructions: 27 | # The clock works by rounding the time to the nearest 5 minutes. 28 | # This means that you need to have settngs for each five minute interval "m00" 29 | # "m00", "m05". 30 | # The clock also works on a 12 hour basis rather than 24 hour: 31 | # "h00", "h01" etc. 32 | # There are three optional parameters: 33 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 34 | # "am": Wording/symbol to indicate morning. 35 | # "pm": Wording/symbol to indicate afternoon/evening 36 | MAP = { 37 | "all": [], 38 | "m00": [], 39 | "m05": [60, 67, 68, 69, 70, 71], 40 | "m10": [60, 75, 76, 77, 78], 41 | "m15": [60, 81, 82, 83, 84, 85, 86], 42 | "m20": [60, 90, 91, 92, 93, 94, 95], 43 | "m25": [60, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118], 44 | "m30": [60, 97, 98, 99, 100, 101], 45 | "m35": [61, 62, 63, 64, 65, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118], 46 | "m40": [61, 62, 63, 64, 65, 90, 91, 92, 93, 94, 95], 47 | "m45": [61, 62, 63, 64, 65, 81, 82, 83, 84, 85, 86], 48 | "m50": [61, 62, 63, 64, 65, 75, 76, 77, 78], 49 | "m55": [61, 62, 63, 64, 65, 67, 68, 69, 70, 71], 50 | "h01": [0, 1, 5, 6, 9, 10, 11], 51 | "h02": [1, 2, 3, 5, 6, 7, 12, 13, 14], 52 | "h03": [1, 2, 3, 5, 6, 7, 15, 16, 17, 18], 53 | "h04": [1, 2, 3, 5, 6, 7, 19, 20, 21, 22, 23, 24], 54 | "h05": [1, 2, 3, 5, 6, 7, 25, 26, 27, 28, 29], 55 | "h06": [1, 2, 3, 5, 6, 7, 30, 31, 32, 33], 56 | "h07": [1, 2, 3, 5, 6, 7, 33, 34, 35, 36, 37], 57 | "h08": [1, 2, 3, 5, 6, 7, 38, 39, 40, 41], 58 | "h09": [1, 2, 3, 5, 6, 7, 45, 46, 47, 48, 49], 59 | "h10": [1, 2, 3, 5, 6, 7, 51, 52, 53, 54], 60 | "h11": [1, 2, 3, 5, 6, 7, 41, 42, 43, 44], 61 | "h12": [1, 2, 3, 5, 6, 7, 56, 57, 58, 59], 62 | "m00": [], 63 | "m05": [60, 67, 68, 69, 70, 71], 64 | "m10": [60, 75, 76, 77, 78], 65 | "m15": [60, 81, 82, 83, 84, 85, 86], 66 | "m20": [60, 90, 91, 92, 93, 94, 95], 67 | "m25": [60, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118], 68 | "m30": [60, 97, 98, 99, 100, 101], 69 | "m35": [61, 62, 63, 64, 65, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118], 70 | "m40": [61, 62, 63, 64, 65, 90, 91, 92, 93, 94, 95], 71 | "m45": [61, 62, 63, 64, 65, 81, 82, 83, 84, 85, 86], 72 | "m50": [61, 62, 63, 64, 65, 75, 76, 77, 78], 73 | "m55": [61, 62, 63, 64, 65, 67, 68, 69, 70, 71], 74 | "am": [], 75 | "pm": [] 76 | } 77 | 78 | # Number of columns in grid layout 79 | COLS = 15 80 | 81 | # Size of letter in grid (x, y) 82 | SIZE = (53, 60) 83 | 84 | # Font size of letter 85 | FONTSIZE = 40 86 | 87 | # Is our language one where we need to increment the hour after 30 mins 88 | # e.g. 9:40 is "Twenty to ten" 89 | HOUR_INCREMENT = True 90 | 91 | HOUR_INCREMENT_TIME = 30 92 | -------------------------------------------------------------------------------- /screens/wordclock/layouts/swedish.py: -------------------------------------------------------------------------------- 1 | # This Python file uses the following encoding: utf-8 2 | '''This is a custom layout for the RPi InfoScreen wordclock screen. 3 | 4 | Custom layouts can be created for the screen by creating a new file in the 5 | "layouts" folder. 6 | 7 | Each layout must have the following variables: 8 | LAYOUT: The grid layout. Must be a single string. 9 | MAP: The mapping required for various times (see notes below) 10 | COLS: The number of columns required for the grid layout 11 | SIZE: The size of the individual box containing your letter. 12 | Tuple in (x, y) format. 13 | FONTSIZE: Font size for the letter 14 | ''' 15 | 16 | # Layout is a single string variable which will be looped over by the parser. 17 | LAYOUT = (u"HONIÄRCUBMWLRPI" 18 | u"ENQKVARTRFDHALV" 19 | u"ELTJUGOFEMEGTIO" 20 | u"IOXÖVERIYTOLVVE" 21 | u"ETTSEXTREEENIOE" 22 | u"SJUENTVÅXELVAEN" 23 | u"ÅTTATIOFYRAFEME" 24 | u"RPIO'CLOCKHFMEM") 25 | 26 | # Map instructions: 27 | # The clock works by rounding the time to the nearest 5 minutes. 28 | # This means that you need to have settngs for each five minute interval "m00" 29 | # "m00", "m05". 30 | # The clock also works on a 12 hour basis rather than 24 hour: 31 | # "h00", "h01" etc. 32 | # There are three optional parameters: 33 | # "all": Anything that is always shown regardless of the time e.g. "It is..." 34 | # "am": Wording/symbol to indicate morning. 35 | # "pm": Wording/symbol to indicate afternoon/evening 36 | MAP = { 37 | "all": [0, 1, 2, 4, 5], 38 | "m00": [], 39 | "m05": [37,38, 39, 48, 49, 50, 51], 40 | "m10": [42, 43, 44, 48, 49, 50, 51], 41 | "m15": [18, 19, 20, 21, 22, 48, 49, 50, 51], 42 | "m20": [32, 33, 34, 35, 36, 48, 49, 50, 51], 43 | "m25": [32, 33, 34, 35, 36, 37, 38, 39, 48, 49, 50, 51], 44 | "m30": [26, 27, 28, 29], 45 | "m35": [32, 33, 34, 35, 36, 37, 38, 39, 52], 46 | "m40": [32, 33, 34, 35, 36, 52], 47 | "m45": [18, 19, 20, 21, 22, 52], 48 | "m50": [42, 43, 44, 52], 49 | "m55": [37, 38, 39, 52], 50 | "h01": [60, 61, 62], 51 | "h02": [80, 81, 82], 52 | "h03": [66, 67, 68], 53 | "h04": [97, 98, 99, 100], 54 | "h05": [101, 102, 103], 55 | "h06": [63, 64, 65], 56 | "h07": [75, 76, 77], 57 | "h08": [90, 91, 92, 93], 58 | "h09": [71, 72, 73], 59 | "h10": [94, 95, 96], 60 | "h11": [84, 85, 86, 87], 61 | "h12": [54, 55, 56, 57], 62 | "am": [116, 117], 63 | "pm": [118, 119] 64 | } 65 | 66 | # Number of columns in grid layout 67 | COLS = 15 68 | 69 | # Size of letter in grid (x, y) 70 | SIZE = (53, 60) 71 | 72 | # Font size of letter 73 | FONTSIZE = 40 74 | 75 | # Is our language one where we need to increment the hour after 30 mins 76 | # e.g. 9:40 is "Twenty to ten" 77 | HOUR_INCREMENT = True 78 | 79 | HOUR_INCREMENT_TIME = 29 80 | -------------------------------------------------------------------------------- /screens/wordclock/screen.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | from datetime import datetime as DT 5 | 6 | from kivy.animation import Animation 7 | from kivy.clock import Clock 8 | from kivy.properties import BooleanProperty, StringProperty, ListProperty 9 | from kivy.uix.gridlayout import GridLayout 10 | from kivy.uix.label import Label 11 | from kivy.uix.screenmanager import Screen 12 | 13 | 14 | def round_down(num, divisor): 15 | return num - (num % divisor) 16 | 17 | 18 | class WordClockLetter(Label): 19 | """Word clock letter object. The colour of the letter is changed by calling 20 | the "toggle" method. 21 | """ 22 | textcol = ListProperty([0.15, 0.15, 0.15, 1]) 23 | 24 | def __init__(self, **kwargs): 25 | super(WordClockLetter, self).__init__(**kwargs) 26 | 27 | # Flag for determining whether the state of the letter has changed 28 | self.oldstate = False 29 | 30 | # Variable for the duration of the animated fade 31 | self.fadetime = 1 32 | 33 | self.off_colour = [0.15, 0.15, 0.15, 1] 34 | self.on_colour = kwargs["colour"] 35 | 36 | def toggle(self, on): 37 | if on: 38 | colour = self.on_colour 39 | else: 40 | colour = self.off_colour 41 | 42 | # Add some animation effect to fade between different times. 43 | if on != self.oldstate: 44 | self.oldstate = on 45 | anim = Animation(textcol=colour, duration=self.fadetime) 46 | anim.start(self) 47 | 48 | 49 | class WordClockScreen(Screen): 50 | def __init__(self, **kwargs): 51 | super(WordClockScreen, self).__init__(**kwargs) 52 | self.running = False 53 | self.timer = None 54 | self.oldtime = None 55 | 56 | # Set up some variables to help load the chosen layout. 57 | self.basepath = os.path.dirname(os.path.abspath(__file__)) 58 | self.layouts = os.path.join(self.basepath, "layouts") 59 | self.lang = kwargs["params"]["language"].lower() 60 | self.colour = self.get_colour(kwargs["params"]["colour"]) 61 | 62 | def get_colour(self, colour): 63 | return [x/255.0 for x in colour] + [1] 64 | 65 | def on_enter(self): 66 | # We only want to set up the screen once 67 | if not self.running: 68 | self.setup() 69 | self.running = True 70 | 71 | # Set the interval timer 72 | self.timer = Clock.schedule_interval(self.update, 1) 73 | 74 | def on_leave(self): 75 | Clock.unschedule(self.timer) 76 | 77 | def update(self, *args): 78 | # What time is it? 79 | nw = DT.now() 80 | hour = nw.hour 81 | minute = round_down(nw.minute, 5) 82 | 83 | # Is our language one where we need to increment the hour after 30 mins 84 | # e.g. 9:40 is "Twenty to ten" 85 | if self.config.HOUR_INCREMENT and (minute > self.config.HOUR_INCREMENT_TIME): 86 | hour += 1 87 | 88 | # Convert rounded time to string 89 | tm = "{:02d}:{:02d}".format(hour, minute) 90 | 91 | # If it's the same as the last update then we don't need to do anything 92 | if tm != self.oldtime: 93 | 94 | # Change to 12 hour clock 95 | if hour == 24: 96 | hour = 0 97 | elif hour > 12: 98 | hour -= 12 99 | 100 | if hour == 0: 101 | hour = 12 102 | 103 | # Morning or afternoon? 104 | ampm = "am" if nw.hour < 12 else "pm" 105 | 106 | # Get necessary key names 107 | h = "h{:02d}".format(hour) 108 | m = "m{:02d}".format(minute) 109 | 110 | # Load the map 111 | d = self.config.MAP 112 | 113 | # Build list of the letters we need 114 | tm = d.get("all", []) + d[h] + d[m] + d.get(ampm, []) 115 | 116 | # Build a map of all letters saying whether they're on or off 117 | st = [x in tm for x in range(len(self.config.LAYOUT))] 118 | 119 | # Create a list of tuples of (letter, state) 120 | updt = zip(self.letters, st) 121 | 122 | # Loop over the list and toggle the letter 123 | for z in updt: 124 | z[0].toggle(z[1]) 125 | 126 | def loadLayout(self): 127 | """Simple method to import the layout. If the module can't be found 128 | then it defaults to loading the English layout. 129 | """ 130 | module = os.path.join(self.layouts, "{}.py".format(self.lang)) 131 | 132 | try: 133 | config = imp.load_source("layouts.{}".format(self.lang), module) 134 | 135 | except ImportError: 136 | self.lang = "english" 137 | config = imp.load_source("layouts.{}".format(self.lang), module) 138 | 139 | return config 140 | 141 | def setup(self): 142 | # Get the layout 143 | self.config = self.loadLayout() 144 | 145 | # We'll want to keep a list of all the letter objects 146 | self.letters = [] 147 | 148 | # Create a grid layout that's the right size 149 | grid = GridLayout(cols=self.config.COLS) 150 | 151 | # Loop over the letters 152 | for ltr in self.config.LAYOUT: 153 | 154 | # Create a letter object 155 | word = WordClockLetter(text=ltr, 156 | size=self.config.SIZE, 157 | font_size=self.config.FONTSIZE, 158 | colour=self.colour) 159 | 160 | # add it to our list... 161 | grid.add_widget(word) 162 | 163 | # ...and to the grid layout 164 | self.letters.append(word) 165 | 166 | # Clear the screen 167 | self.clear_widgets() 168 | 169 | # add the clock layout 170 | self.add_widget(grid) 171 | -------------------------------------------------------------------------------- /screens/wordclock/wordclock.kv: -------------------------------------------------------------------------------- 1 | 2 | size_hint: None, None 3 | color: self.textcol 4 | 5 | : 6 | Label: 7 | pos: 0, 0 8 | text: "Loading Word Clock." 9 | -------------------------------------------------------------------------------- /screens/xmas/README: -------------------------------------------------------------------------------- 1 | A plugin has the following structure 2 | 3 | /FolderName (Must be unique) 4 | |--> conf.json 5 | |--> screen.py 6 | |--> NAME.kv 7 | 8 | 1) FolderName 9 | 10 | Must be unique as this is the identifier used by the master script to handle the different screens. 11 | 12 | 2) conf.json 13 | 14 | File must have this name. 15 | 16 | JSON formatted file. Has two mandatory parameters: 17 | "screen" - the class name of the main screen for the plugin. 18 | "kv" - the name of the kv file (including extension) which contains the layout for your plugin. 19 | 20 | Optional parameters: 21 | "enabled" - plugins can be disabled by setting this to false. 22 | "params" - any specific parameters to be passed to your plugin 23 | "dependencies" - list any non-standard libraries that are needed to run your plugin. If these aren't installed then the user will be notified. 24 | 25 | 3) screen.py 26 | 27 | File must have this name. 28 | 29 | Main python file for your plugin. Must have a class definition matching the "screen" parameter in the conf.json file. 30 | 31 | The class must inherit the Kivy Screen class. 32 | 33 | 4) NAME.kv 34 | 35 | This file can be called anything you like as long as it's name matches the name set in the conf.json file. 36 | -------------------------------------------------------------------------------- /screens/xmas/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "screen": "XmasScreen", 3 | "kv": "xmas.kv", 4 | "dependencies": [], 5 | "enabled": false 6 | } 7 | -------------------------------------------------------------------------------- /screens/xmas/screen.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from kivy.clock import Clock 4 | from kivy.properties import StringProperty 5 | from kivy.uix.boxlayout import BoxLayout 6 | from kivy.uix.label import Label 7 | from kivy.uix.screenmanager import Screen 8 | 9 | class XmasScreen(Screen): 10 | 11 | first_line = StringProperty("") 12 | second_line = StringProperty("") 13 | third_line = StringProperty("") 14 | 15 | def __init__(self, **kwargs): 16 | super(XmasScreen, self).__init__(**kwargs) 17 | self.xmas = self.getChristmas() 18 | self.first_line = "There are..." 19 | self.second_line = "[calculating time...]" 20 | self.third_line = "until Christmas!" 21 | self.timer = None 22 | 23 | def getChristmas(self): 24 | nw = datetime.now() 25 | if nw.month == 12 and nw.day > 25: 26 | yr = nw.year + 1 27 | else: 28 | yr = nw.year 29 | 30 | return datetime(yr, 12, 25, 0, 0) 31 | 32 | def on_enter(self): 33 | self.timer = Clock.schedule_interval(self.update, 1) 34 | 35 | def on_leave(self): 36 | Clock.unschedule(self.timer) 37 | 38 | def update(self, *args): 39 | nw = datetime.now() 40 | 41 | delta = self.xmas - nw 42 | 43 | if delta.total_seconds() < 0: 44 | # It's Christmas 45 | 46 | self.first_line = "" 47 | self.second_line = "[size=100]Happy Christmas![/size]" 48 | self.third_line = "" 49 | 50 | else: 51 | 52 | d = delta.days 53 | h, rem = divmod(delta.seconds, 3600) 54 | m, _ = divmod(rem, 60) 55 | 56 | self.first_line = "There are..." 57 | self.second_line = ("[size=30][size=70]{d}[/size] days, " 58 | "[size=60]{h}[/size] hours and " 59 | "[size=60]{m}[/size] " 60 | "minutes[/size]").format(d=d, h=h, m=m) 61 | self.third_line = "...until Christmas." 62 | -------------------------------------------------------------------------------- /screens/xmas/xmas.kv: -------------------------------------------------------------------------------- 1 | : 2 | BoxLayout: 3 | orientation: "vertical" 4 | 5 | Label: 6 | size_hint_y: 0.25 7 | text_size: self.size 8 | text: root.first_line 9 | font_size: 30 10 | halign: "left" 11 | valign: "middle" 12 | padding: 20, 0 13 | 14 | Label: 15 | size_hint_y: 0.5 16 | text: root.second_line 17 | markup: True 18 | 19 | Label: 20 | size_hint_y: 0.25 21 | text_size: self.size 22 | text: root.third_line 23 | font_size: 30 24 | halign: "right" 25 | valign: "middle" 26 | padding: 20, 0 27 | -------------------------------------------------------------------------------- /web/templates/all_screens.tpl: -------------------------------------------------------------------------------- 1 | % rebase("base.tpl", title="Installed Screens") 2 | 3 | 4 | 5 | % for screen in screens: 6 | 7 | 8 | 13 | 18 | 23 | 25 | 30 | 31 | % end 32 |
{{screen.capitalize()}}
33 | 34 | -------------------------------------------------------------------------------- /web/templates/base.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 51 | 52 | Raspberry Pi Information Screen - Web Interface 53 | 54 | 55 | 56 | 57 |

Raspberry Pi Information Screen

58 |

{{title}}


59 | {{!base}} 60 | 61 | 62 | 63 | --------------------------------------------------------------------------------