omegalines 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import HTMLParser
  4. import datetime as dt
  5. import dateutil.parser
  6. import dateutil.tz
  7. import json
  8. import os
  9. import time
  10. import urllib2
  11. import yaml
  12. from OmegaExpansion import oledExp
  13. DEFAULT_CONFIG_PATHS = [
  14. os.path.join(os.path.expanduser('~'), '.omegalines'),
  15. os.path.join(os.sep, 'etc', 'omegalines'),
  16. ]
  17. OLED_DISPLAY_HEIGHT = 8
  18. OLED_DISPLAY_WIDTH = 21
  19. WIENER_LINIEN_DEFAULT_UPDATE_INTERVAL_SECONDS = 10
  20. OEBB_DEFAULT_UPDATE_INTERVAL_SECONDS = 30
  21. # https://openwrt.org/docs/user-guide/system_configuration
  22. OEBB_TIMEZONE = dateutil.tz.tzstr('CET-1CEST,M3.5.0,M10.5.0/3')
  23. html_parser = HTMLParser.HTMLParser()
  24. def datetime_now_local():
  25. return dt.datetime.now(dateutil.tz.tzlocal())
  26. def format_timedelta(timedelta):
  27. total_seconds = timedelta.total_seconds()
  28. return '%s%d:%02d' % (
  29. '-' if total_seconds < 0 else '',
  30. int(abs(total_seconds) / 60),
  31. abs(total_seconds) % 60,
  32. )
  33. assert "0:20" == format_timedelta(dt.timedelta(seconds=20))
  34. assert "1:20" == format_timedelta(dt.timedelta(seconds=80))
  35. assert "2:00" == format_timedelta(dt.timedelta(seconds=120))
  36. assert "-0:20" == format_timedelta(dt.timedelta(seconds=-20))
  37. assert "-1:20" == format_timedelta(dt.timedelta(seconds=-80))
  38. assert "-2:00" == format_timedelta(dt.timedelta(seconds=-120))
  39. def parse_oebb_datetime(date_str, time_str):
  40. return dt.datetime.combine(
  41. dt.datetime.strptime(date_str, '%d.%m.%Y').date(),
  42. dt.datetime.strptime(time_str, '%H:%M').time().replace(second=0),
  43. ).replace(tzinfo=OEBB_TIMEZONE)
  44. assert '2018-02-22T09:46:00+01:00' == \
  45. parse_oebb_datetime(u'22.02.2018', u'09:46').isoformat()
  46. def oled_write_line(line):
  47. oledExp.write(
  48. line.ljust(OLED_DISPLAY_WIDTH, ' ')[:OLED_DISPLAY_WIDTH],
  49. )
  50. def oled_encode(text):
  51. return text.replace(u'ä', u'ae') \
  52. .replace(u'ö', u'oe') \
  53. .replace(u'ü', u'ue')
  54. class Departure:
  55. def __init__(self, line, towards, predicted_time):
  56. self.line = line
  57. self.towards = towards
  58. self.predicted_time = predicted_time
  59. @property
  60. def predicted_timedelta(self):
  61. return self.predicted_time - datetime_now_local()
  62. def request_wiener_linien_departures(api_key, rbl):
  63. req = urllib2.Request(
  64. "https://www.wienerlinien.at/ogd_realtime/monitor?sender=%s&rbl=%s"
  65. % (api_key, rbl),
  66. )
  67. req.add_header("Accept", "application/json")
  68. req.add_header("Content-Type", "application/json")
  69. req_time = datetime_now_local()
  70. resp = urllib2.urlopen(req)
  71. resp_data = json.loads(resp.read())
  72. # dt.datetime.strptime:
  73. # ValueError: 'z' is a bad directive in format
  74. # '%Y-%m-%dT%H:%M:%S.%f%z'
  75. server_time_delta = req_time - \
  76. dateutil.parser.parse(resp_data['message']['serverTime'])
  77. monitors_data = resp_data['data']['monitors']
  78. assert 1 == len(monitors_data)
  79. departures = []
  80. for line_data in monitors_data[0]['lines']:
  81. assert 1 == len(line_data['departures'])
  82. for departure_data in line_data['departures']['departure']:
  83. try:
  84. predicted_time_server = dateutil.parser.parse(
  85. departure_data['departureTime']['timeReal'],
  86. )
  87. except KeyError as e:
  88. print(e)
  89. predicted_time_server = None
  90. if predicted_time_server:
  91. departures.append(Departure(
  92. line=departure_data['vehicle']['name']
  93. if 'vehicle' in departure_data else line_data['name'],
  94. towards=departure_data['vehicle']['towards']
  95. if 'vehicle' in departure_data else line_data['towards'],
  96. predicted_time=predicted_time_server - server_time_delta,
  97. ))
  98. return departures
  99. def request_oebb_departures(eva_id):
  100. req_time = datetime_now_local()
  101. req = urllib2.Request(
  102. 'http://fahrplan.oebb.at/bin/stboard.exe/dn?' + '&'.join([
  103. 'L=vs_scotty.vs_liveticker',
  104. 'evaId=%d' % eva_id,
  105. 'boardType=dep',
  106. 'disableEquivs=yes',
  107. 'outputMode=tickerDataOnly',
  108. 'start=yes',
  109. ]),
  110. )
  111. print('request %s' % req.get_full_url())
  112. resp = urllib2.urlopen(req)
  113. resp_data = json.loads(
  114. resp.read().replace('journeysObj = ', ''),
  115. )
  116. departures = []
  117. for departure_data in resp_data.get('journey', []):
  118. """
  119. pr: line (u'R 2323')
  120. lastStop (u'Wr.Neustadt Hbf)
  121. da: planned departure date (u'22.02.2018')
  122. ti: planned departure time (u'09:42')
  123. rt: dict if delayed, otherwise False
  124. rt.dld: estimated departure date
  125. rt.dlt: estimated departure time
  126. """
  127. if departure_data['rt']:
  128. predicted_time = parse_oebb_datetime(
  129. departure_data['rt']['dld'],
  130. departure_data['rt']['dlt'],
  131. )
  132. else:
  133. predicted_time = parse_oebb_datetime(
  134. departure_data['da'],
  135. departure_data['ti'],
  136. )
  137. departures.append(Departure(
  138. line=departure_data['pr'],
  139. towards=html_parser.unescape(departure_data['lastStop']),
  140. predicted_time=predicted_time,
  141. ))
  142. return departures
  143. def draw_departures(departures):
  144. oledExp.setCursor(0, 0)
  145. oledExp.write(datetime_now_local().strftime("%Y-%m-%d %H:%M:%S"))
  146. departures.sort(key=lambda d: d.predicted_time)
  147. for departure_idx, departure in enumerate(departures[:OLED_DISPLAY_HEIGHT - 1]):
  148. oledExp.setCursor(1 + departure_idx, 0)
  149. oled_write_line("%s %s %s" % (
  150. format_timedelta(departure.predicted_timedelta),
  151. departure.line.replace(' ', ''),
  152. oled_encode(departure.towards),
  153. ))
  154. def run(config_path):
  155. if config_path is None:
  156. available_config_paths = [
  157. p for p in DEFAULT_CONFIG_PATHS if os.path.exists(p)
  158. ]
  159. if len(available_config_paths) == 0:
  160. raise Exception('found no config file')
  161. config_path = available_config_paths[0]
  162. print('config path: %s' % config_path)
  163. with open(config_path, 'r') as config_file:
  164. config = yaml.load(config_file.read())
  165. if not 'update_interval_seconds' in config['wiener_linien']:
  166. config['wiener_linien']['update_interval_seconds'] = \
  167. WIENER_LINIEN_DEFAULT_UPDATE_INTERVAL_SECONDS
  168. if not 'oebb' in config:
  169. config['oebb'] = {}
  170. if not 'update_interval_seconds' in config['oebb']:
  171. config['oebb']['update_interval_seconds'] = \
  172. OEBB_DEFAULT_UPDATE_INTERVAL_SECONDS
  173. assert not oledExp.driverInit()
  174. assert not oledExp.setDisplayPower(1)
  175. wiener_linien_departures = []
  176. wiener_linien_last_update_time = None
  177. oebb_departures = []
  178. oebb_last_update_time = None
  179. while True:
  180. if wiener_linien_last_update_time is None \
  181. or time.time() - wiener_linien_last_update_time \
  182. > config['wiener_linien']['update_interval_seconds']:
  183. print('update wiener linien')
  184. try:
  185. wiener_linien_departures = request_wiener_linien_departures(
  186. api_key=config['wiener_linien']['api_key'],
  187. rbl=config['wiener_linien']['rbl'],
  188. )
  189. wiener_linien_last_update_time = time.time()
  190. except urllib2.HTTPError as e:
  191. print(e)
  192. if 'eva_ids' in config['oebb'] \
  193. and (oebb_last_update_time is None
  194. or time.time() - oebb_last_update_time
  195. > config['oebb']['update_interval_seconds']):
  196. oebb_departures = []
  197. for eva_id in config['oebb']['eva_ids']:
  198. oebb_departures.extend(request_oebb_departures(eva_id))
  199. oebb_last_update_time = time.time()
  200. departures = wiener_linien_departures + oebb_departures
  201. if 'offset_seconds' in config:
  202. current_time = datetime_now_local()
  203. departures = filter(
  204. lambda d: (d.predicted_time - current_time).total_seconds()
  205. >= config['offset_seconds'],
  206. departures,
  207. )
  208. draw_departures(departures)
  209. time.sleep(0.1)
  210. def _init_argparser():
  211. import argparse
  212. argparser = argparse.ArgumentParser()
  213. argparser.add_argument(
  214. '-c', '--config-path',
  215. dest='config_path',
  216. type=str,
  217. default=None,
  218. help='default: %r' % DEFAULT_CONFIG_PATHS,
  219. )
  220. return argparser
  221. def main(argv):
  222. argparser = _init_argparser()
  223. args = argparser.parse_args(argv)
  224. run(**vars(args))
  225. return 0
  226. if __name__ == "__main__":
  227. import sys
  228. sys.exit(main(sys.argv[1:]))