btcticker

eInk Bitcoin price ticker
git clone https://git.parazyd.org/btcticker
Log | Files | Refs | README | LICENSE

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