tticker.py - btcticker - eInk Bitcoin price ticker
HTML git clone https://git.parazyd.org/btcticker
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
tticker.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())