Websocket server implementing a clock handling alarms and timezones
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env python3
  2. import argparse
  3. import asyncio
  4. import logging
  5. from logging.handlers import WatchedFileHandler
  6. import websockets
  7. from .command import Command
  8. from .config import parse_config
  9. from .server import websocket_handler
  10. from .session import Session
  11. async def main(listen_addr:str, port:int):
  12. """ Create the websocket and handle new connection with websocket_handler
  13. """
  14. logging.warning('Listening on %s:%d' % (listen_addr, port))
  15. async with websockets.serve(websocket_handler, listen_addr, port):
  16. await asyncio.Future()
  17. def cli():
  18. """ Run CLI """
  19. proto_desc = '''
  20. Before entering the main loop, the listening server waits for 1st message
  21. containing a session ID. If an empty/dummy/invalid session ID is sent by the
  22. client, the server replies with a new, valid, session ID and enter the main loop.
  23. If a valid session ID is sent, the server reply the same session ID and enter
  24. the main loop, formated using SESSION:<SESSION_ID>.
  25. Main loop :
  26. -----------
  27. The server send the current time (ISO 8601 format) every second.
  28. Command results :
  29. -----------------
  30. The client can interact with the server sending one of the command listed using
  31. the -L flag.
  32. When a client send a command, the next message from the server will be a command
  33. result. Results are string formatted using <STATUS>:<DETAILS>. Status can be
  34. one of 'ERR' or 'OK', details are optionnale for OK statuses.
  35. Alarms rings :
  36. --------------
  37. When an alarm is ringing, after sending time, the server send messages formatted
  38. using ALRM:<NAME> where NAME is the name of a ringing alarm. If multiple alarms
  39. are ringing, multiple ALRM: messages are sent every seconds.
  40. Timezone informations :
  41. -----------------------
  42. Before the first time is sent and every time the timezone changes (inclucing
  43. summer/winter transitions), the clock. Timezones messages are formatted using
  44. TZN:<TZNAME>
  45. Messages types summary :
  46. ------------------------
  47. Session ID
  48. SESSION:<SESSION_ID>
  49. Clock tick
  50. ISO8601_Datetime
  51. Timezone name
  52. TZN:<TZNAME>
  53. Alarm rings
  54. ALRM:<JSON_ARRAY>
  55. Command results
  56. OK:<DETAILS>
  57. ERR:<REASON>
  58. '''
  59. parser = argparse.ArgumentParser(description='Run a simple clock websocket \
  60. server sending time every seconds', epilog=proto_desc,
  61. formatter_class=argparse.RawDescriptionHelpFormatter)
  62. parser.add_argument('-C', '--config', default=None,
  63. help="Indicate a config.ini file")
  64. parser.add_argument('-p', '--port', default=80, type=int,
  65. help='The TCP port to listen to')
  66. parser.add_argument('-l', '--listen-address', default='localhost', type=str,
  67. help='Bind to this address (localhost by default)')
  68. parser.add_argument('-L', '--list-commands', default=False,
  69. action='store_true',
  70. help='List available commands on the websocket and exit')
  71. parser.add_argument('-H', '--help-command', metavar='CMD',
  72. choices=Command.list()+['ALL'],
  73. help='Print a command help and exit')
  74. parser.add_argument('-v', '--verbose', action='count', default=0,
  75. help='Increase verbosity level')
  76. parser.add_argument('-F', '--file-session', action="store_true", default=False,
  77. help="Use persistent file session instead of RAM session")
  78. parser.add_argument('-S', '--session-dir', type=str, default=None,
  79. help="Store session in given directory (implies -F)")
  80. parser.add_argument('--logfile', type=str,
  81. help="Log to given file instead of stderr")
  82. args = parser.parse_args()
  83. if args.config:
  84. parse_config(args.config, args)
  85. loglvls = [logging.WARN, logging.INFO, logging.DEBUG]
  86. if args.verbose >= len(loglvls):
  87. lvl = loglvls[-1]
  88. else:
  89. lvl = loglvls[args.verbose]
  90. bconfig_kwargs = {'level':lvl,
  91. 'format':'%(asctime)s %(levelname)s:%(message)s'}
  92. if args.logfile:
  93. bconfig_kwargs['handlers'] = [WatchedFileHandler(args.logfile)]
  94. logging.basicConfig(**bconfig_kwargs)
  95. if args.file_session or args.session_dir:
  96. Session.set_file_handler(args.session_dir)
  97. all_cmd = Command.all()
  98. if args.list_commands:
  99. for cmd in all_cmd.values():
  100. usage = ':'.join(cmd.usage.split(':')[1:]).strip()
  101. print('# %s\n\t%s' % (cmd.description, usage))
  102. exit(0)
  103. if args.help_command:
  104. if args.help_command == 'ALL':
  105. print(('-'*16).join([cmd.help
  106. for cmd in all_cmd.values()]))
  107. exit(0)
  108. if args.help_command not in all_cmd:
  109. logging.critical('Unknown command %r' % args.help_command)
  110. exit(1)
  111. print(all_cmd[args.help_command].help)
  112. exit(0)
  113. asyncio.run(main(args.listen_address, args.port))
  114. if __name__ == '__main__':
  115. cli()