ticker.py (7460B)
1 #!/usr/bin/env python3 2 # Copyright (c) 2021 parazyd 3 # Copyright (c) 2020 llvll 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 # SOFTWARE. 22 """ 23 Program to create a bitcoin ticker bitmap and show it on a waveshare 2in13_V2 24 eink screen (https://www.waveshare.com/wiki/2.13inch_e-Paper_HAT) 25 """ 26 27 import sys 28 from io import BytesIO 29 from json.decoder import JSONDecodeError 30 from os.path import join, dirname, realpath 31 from time import time, strftime, sleep 32 import matplotlib as mpl 33 import matplotlib.pyplot as plt 34 import numpy as np 35 from PIL import Image, ImageFont, ImageDraw 36 import requests 37 38 mpl.use('Agg') 39 40 picdir = join(dirname(realpath(__file__)), 'images') 41 fontdir = join(dirname(realpath(__file__)), 'fonts') 42 font = ImageFont.truetype(join(fontdir, 'googlefonts/Roboto-Medium.ttf'), 40) 43 font_date = ImageFont.truetype(join(fontdir, 'PixelSplitter-Bold.ttf'), 11) 44 45 pairs = [ 46 { 47 "currency": "usd", 48 "coin": "bitcoin", 49 "image": Image.open(join(picdir, "currency/bitcoin.png")), 50 }, 51 { 52 "currency": "usd", 53 "coin": "dogecoin", 54 "image": Image.open(join(picdir, "currency/dogecoin.png")), 55 }, 56 { 57 "currency": "usd", 58 "coin": "ethereum", 59 "image": Image.open(join(picdir, "currency/ethereum.png")), 60 }, 61 { 62 "currency": "usd", 63 "coin": "litecoin", 64 "image": Image.open(join(picdir, "currency/litecoin.png")), 65 }, 66 { 67 "currency": "usd", 68 "coin": "monero", 69 "image": Image.open(join(picdir, "currency/monero.png")), 70 }, 71 { 72 "currency": "usd", 73 "coin": "solana", 74 "image": Image.open(join(picdir, "currency/solana.png")), 75 }, 76 { 77 "currency": "usd", 78 "coin": "zcash", 79 "image": Image.open(join(picdir, "currency/zcash.png")), 80 }, 81 ] 82 83 athbitmap = Image.open(join(picdir, 'ATH.bmp')) 84 API = 'https://api.coingecko.com/api/v3/coins' 85 86 87 def get_data(pair, other): 88 """ Grab data from API """ 89 days_ago = 7 90 endtime = int(time()) 91 starttime = endtime - 60 * 60 * 24 * days_ago 92 93 geckourl = '%s/markets?vs_currency=%s&ids=%s' % (API, pair["currency"], 94 pair["coin"]) 95 liveprice = requests.get(geckourl).json()[0] 96 pricenow = float(liveprice['current_price']) 97 alltimehigh = float(liveprice['ath']) 98 other['volume'] = float(liveprice['total_volume']) 99 100 url_hist = '%s/%s/market_chart/range?vs_currency=%s&from=%s&to=%s' % ( 101 API, pair["coin"], pair["currency"], str(starttime), str(endtime)) 102 103 try: 104 timeseriesarray = requests.get(url_hist).json()['prices'] 105 except JSONDecodeError as err: 106 print(f'Caught JSONDecodeError: {repr(err)}') 107 return None 108 timeseriesstack = [] 109 length = len(timeseriesarray) 110 i = 0 111 while i < length: 112 timeseriesstack.append(float(timeseriesarray[i][1])) 113 i += 1 114 115 timeseriesstack.append(pricenow) 116 if pricenow > alltimehigh: 117 other['ATH'] = True 118 else: 119 other['ATH'] = False 120 121 other["image"] = pair["image"] 122 other["coin"] = pair["coin"] 123 124 return timeseriesstack 125 126 127 def make_spark(pricestack): 128 """ Make a historical plot """ 129 _x = pricestack - np.mean(pricestack) 130 fig, _ax = plt.subplots(1, 1, figsize=(10, 3)) 131 plt.plot(_x, color='k', linewidth=6) 132 plt.plot(len(_x) - 1, _x[-1], color='r', marker='o') 133 134 for _, i in _ax.spines.items(): 135 i.set_visible(False) 136 _ax.set_xticks = ([]) 137 _ax.set_yticks = ([]) 138 _ax.axhline(c='k', linewidth=4, linestyle=(0, (5, 2, 1, 2))) 139 140 buf = BytesIO() 141 plt.savefig(buf, format='png', dpi=17) 142 buf.seek(0) 143 imgspk = Image.open(buf) 144 145 plt.clf() 146 _ax.cla() 147 plt.close(fig) 148 return imgspk 149 150 151 def update_display(pricestack, sparkimage, other): 152 """ Create an image from the data and send it to the display """ 153 days_ago = 7 154 pricenow = pricestack[-1] 155 156 if not other.get('lastprice'): 157 other['lastprice'] = pricenow 158 elif pricenow == other['lastprice']: 159 return other 160 161 other['lastprice'] = pricenow 162 163 pricechange = str('%+d' % round( 164 (pricestack[-1] - pricestack[0]) / pricestack[-1] * 100, 2)) + '%' 165 if pricenow > 1000: 166 pricenowstring = format(int(pricenow), ',') 167 else: 168 pricenowstring = str(float('%.5g' % pricenow)) 169 170 image = Image.new('L', (250, 122), 255) 171 draw = ImageDraw.Draw(image) 172 if other['ATH'] is True: 173 print(f"{other['coin']}: {pricenowstring} (ATH!)") 174 image.paste(athbitmap, (15, 30)) 175 else: 176 print(f"{other['coin']}: {pricenowstring}") 177 image.paste(other["image"], (5, 12)) 178 179 draw.text((8, 92), other["coin"], font=font_date, fill=0) 180 181 image.paste(sparkimage, (80, 15)) 182 draw.text((130, 66), 183 str(days_ago) + 'day : ' + pricechange, 184 font=font_date, 185 fill=0) 186 draw.text((96, 73), '$' + pricenowstring, font=font, fill=0) 187 188 draw.text((95, 5), 189 str(strftime('%H:%M %a %d %b %Y')), 190 font=font_date, 191 fill=0) 192 193 display_eink(image) 194 195 return other 196 197 198 def display_eink(image): 199 """ Wrapper to display or show image """ 200 if epd: 201 epd.display(epd.getbuffer(image)) 202 else: 203 image.show() 204 205 206 def close_epd(): 207 """ Cleanup for epd context """ 208 if not epd: 209 return 210 epd.sleep() 211 epd.Dev_exit() 212 # epd2in13_V2.epdconfig.module_exit() 213 214 215 def main(): 216 """ main routine """ 217 218 def fullupdate(pair): 219 other = {} 220 pricestack = get_data(pair, other) 221 if not pricestack: 222 return time() 223 sparkimage = make_spark(pricestack) 224 other = update_display(pricestack, sparkimage, other) 225 return time() 226 227 try: 228 data_pulled = False 229 lastcoinfetch = time() 230 231 while True: 232 for i in pairs: 233 if (time() - lastcoinfetch > float(40)) or data_pulled is False: 234 lastcoinfetch = fullupdate(i) 235 data_pulled = True 236 sleep(5) 237 except KeyboardInterrupt: 238 close_epd() 239 return 1 240 241 return 1 242 243 244 if __name__ == '__main__': 245 if len(sys.argv) > 1 and sys.argv[1] == '-d': 246 epd = None # pylint: disable=C0103 247 else: 248 from waveshare_epd import epd2in13_V2 # pylint: disable=E0401 249 epd = epd2in13_V2.EPD() 250 epd.init(epd.FULL_UPDATE) 251 epd.Clear(0xFF) 252 sys.exit(main())