← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~trb143/openlp/websockets into lp:openlp

 

Tim Bentley has proposed merging lp:~trb143/openlp/websockets into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~trb143/openlp/websockets/+merge/296581

Add WebSockets code remove the web page pooling.
Remove SSL from Remote plugin
Start to add "api" to core.

Unable to run CI as jenkins server does not have asyncio package, 
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/websockets into lp:openlp.
=== modified file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py	2016-05-18 18:40:27 +0000
+++ openlp/core/common/settings.py	2016-06-06 16:44:43 +0000
@@ -213,7 +213,9 @@
         ('media/players', 'media/players_temp', [(media_players_conv, None)]),  # Convert phonon to system
         ('media/players_temp', 'media/players', []),  # Move temp setting from above to correct setting
         ('advanced/default color', 'core/logo background color', []),  # Default image renamed + moved to general > 2.4.
-        ('advanced/default image', '/core/logo file', [])  # Default image renamed + moved to general after 2.4.
+        ('advanced/default image', '/core/logo file', []),  # Default image renamed + moved to general after 2.4.
+        ('remotes/https enabled', '', []),
+        ('remotes/https port', '', [])
     ]
 
     @staticmethod

=== added directory 'openlp/core/lib/api'
=== added file 'openlp/core/lib/api/__init__.py'
--- openlp/core/lib/api/__init__.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/api/__init__.py	2016-06-06 16:44:43 +0000
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+from .poll import OpenLPPoll
+from .wsserver import OpenWSServer
+from .httpserver import OpenLPHttpServer
+from .apicontroller import ApiController
+
+__all__ = ['OpenLPPoll', 'RemoteController', 'OpenLPHttpServer']

=== added file 'openlp/core/lib/api/httpserver.py'
--- openlp/core/lib/api/httpserver.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/api/httpserver.py	2016-06-06 16:44:43 +0000
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+"""
+The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
+with OpenLP. It uses JSON to communicate with the remotes.
+"""
+
+import logging
+from waitress import serve
+
+from PyQt5 import QtCore
+
+from openlp.core.common import RegistryProperties, OpenLPMixin
+
+log = logging.getLogger(__name__)
+
+
+class HttpThread(QtCore.QThread):
+    """
+    A special Qt thread class to allow the HTTP server to run at the same time as the UI.
+    """
+    def __init__(self, server):
+        """
+        Constructor for the thread class.
+
+        :param server: The http server class.
+        """
+        super(HttpThread, self).__init__(None)
+        self.http_server = server
+
+    def run(self):
+        """
+        Run the thread.
+        """
+        wsgiapp = object()
+        serve(wsgiapp, host='0.0.0.0', port=4317)
+
+    def stop(self):
+        self.http_server.stop = True
+
+
+class OpenLPHttpServer(RegistryProperties, OpenLPMixin):
+    """
+    Wrapper round a server instance
+    """
+    def __init__(self, secure=False):
+        """
+        Initialise the http server, and start the server of the correct type http / https
+        """
+        super(OpenLPHttpServer, self).__init__()
+        self.http_thread = HttpThread(self)
+        self.http_thread.start()

=== added file 'openlp/core/lib/api/poll.py'
--- openlp/core/lib/api/poll.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/api/poll.py	2016-06-06 16:44:43 +0000
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+import json
+
+from openlp.core.common import RegistryProperties, Settings
+
+
+class OpenLPPoll(RegistryProperties):
+    """
+    Access by the web layer to get status type information from the application
+    """
+    def __init__(self):
+        """
+        Constructor for the poll builder class.
+        """
+        super(OpenLPPoll, self).__init__()
+
+    def poll(self):
+        """
+        Poll OpenLP to determine the current slide number and item name.
+        """
+        result = {
+            'service': self.service_manager.service_id,
+            'slide': self.live_controller.selected_row or 0,
+            'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
+            'twelve': Settings().value('remotes/twelve hour'),
+            'blank': self.live_controller.blank_screen.isChecked(),
+            'theme': self.live_controller.theme_screen.isChecked(),
+            'display': self.live_controller.desktop_screen.isChecked(),
+            'version': 2,
+            'isSecure': Settings().value('remotes/authentication enabled'),
+            'isAuthorised': False
+        }
+        return json.dumps({'results': result}).encode()
+
+    def main_poll(self):
+        """
+        Poll OpenLP to determine the current slide count.
+        """
+        result = {
+            'slide_count': self.live_controller.slide_count
+        }
+        return json.dumps({'results': result}).encode()

=== added file 'openlp/core/lib/api/wsserver.py'
--- openlp/core/lib/api/wsserver.py	1970-01-01 00:00:00 +0000
+++ openlp/core/lib/api/wsserver.py	2016-06-06 16:44:43 +0000
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+"""
+The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
+with OpenLP. It uses JSON to communicate with the remotes.
+"""
+
+import asyncio
+import websockets
+import logging
+import time
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Settings, RegistryProperties, OpenLPMixin, Registry
+
+log = logging.getLogger(__name__)
+
+
+class HttpThread(QtCore.QThread):
+    """
+    A special Qt thread class to allow the HTTP server to run at the same time as the UI.
+    """
+    def __init__(self, server):
+        """
+        Constructor for the thread class.
+
+        :param server: The http server class.
+        """
+        super(HttpThread, self).__init__(None)
+        self.http_server = server
+
+    def run(self):
+        """
+        Run the thread.
+        """
+        self.http_server.start_server()
+
+    def stop(self):
+        self.http_server.stop = True
+
+
+class OpenWSServer(RegistryProperties, OpenLPMixin):
+    """
+    Wrapper round a server instance
+    """
+    def __init__(self, secure=False):
+        """
+        Initialise the http server, and start the server of the correct type http / https
+        """
+        super(OpenWSServer, self).__init__()
+        self.settings_section = 'remotes'
+        self.secure = secure
+        self.http_thread = HttpThread(self)
+        self.http_thread.start()
+
+    def start_server(self):
+        """
+        Start the correct server and save the handler
+        """
+        address = Settings().value(self.settings_section + '/ip address')
+        port = '4318'
+        self.start_websocket_instance(address, port)
+        # If web socket server start listening
+        if hasattr(self, 'ws_server') and self.ws_server:
+            event_loop = asyncio.new_event_loop()
+            asyncio.set_event_loop(event_loop)
+            event_loop.run_until_complete(self.ws_server)
+            event_loop.run_forever()
+        else:
+            log.debug('Failed to start ws server on port {port}'.format(port=port))
+
+    def start_websocket_instance(self, address, port):
+        """
+        Start the server
+
+        :param address: The server address
+        :param port: The run port
+        """
+        loop = 1
+        while loop < 4:
+            try:
+                self.ws_server = websockets.serve(self.handle_websocket, address, port)
+                log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port))
+                break
+            except Exception as e:
+                log.error('Failed to start ws server {why}'.format(why=e))
+                loop += 1
+                time.sleep(0.1)
+
+    @staticmethod
+    async def handle_websocket(request, path):
+        """
+        Handle web socket requests and return the poll information.
+        Check ever 0.5 seconds to get the latest postion and send if changed.
+        Only gets triggered when 1st client attaches
+        :param request: request from client
+        :param path: not used - future to register for a different end point
+        :return:
+        """
+        log.debug("web socket handler registered with client")
+        previous_poll = None
+        previous_main_poll = None
+        openlppoll = Registry().get('OpenLPPoll')
+        if path == '/poll':
+            while True:
+                current_poll = openlppoll.poll()
+                if current_poll != previous_poll:
+                    await request.send(current_poll)
+                    previous_poll = current_poll
+                await asyncio.sleep(0.2)
+        elif path == '/main_poll':
+            while True:
+                main_poll = openlppoll.main_poll()
+                if main_poll != previous_main_poll:
+                    await request.send(main_poll)
+                    previous_main_poll = main_poll
+                await asyncio.sleep(0.2)
+
+    def stop_server(self):
+        """
+        Stop the server
+        """
+        if self.http_thread.isRunning():
+            self.http_thread.stop()
+        self.httpd = None
+        log.debug('Stopped the server.')

=== modified file 'openlp/core/ui/mainwindow.py'
--- openlp/core/ui/mainwindow.py	2016-05-20 16:22:06 +0000
+++ openlp/core/ui/mainwindow.py	2016-06-06 16:44:43 +0000
@@ -40,13 +40,13 @@
 from openlp.core.common.versionchecker import get_application_version
 from openlp.core.lib import Renderer, PluginManager, ImageManager, PluginStatus, ScreenList, build_icon
 from openlp.core.lib.ui import UiStrings, create_action
+from openlp.core.lib.api import ApiController
 from openlp.core.ui import AboutForm, SettingsForm, ServiceManager, ThemeManager, LiveController, PluginForm, \
     ShortcutListForm, FormattingTagForm, PreviewController
 from openlp.core.ui.firsttimeform import FirstTimeForm
 from openlp.core.ui.media import MediaController
 from openlp.core.ui.printserviceform import PrintServiceForm
 from openlp.core.ui.projector.manager import ProjectorManager
-from openlp.core.ui.lib.toolbar import OpenLPToolbar
 from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
 from openlp.core.ui.lib.mediadockmanager import MediaDockManager
 
@@ -529,6 +529,7 @@
         Settings().set_up_default_values()
         self.about_form = AboutForm(self)
         MediaController()
+        ApiController()
         SettingsForm(self)
         self.formatting_tag_form = FormattingTagForm(self)
         self.shortcut_form = ShortcutListForm(self)

=== modified file 'openlp/core/ui/media/mediacontroller.py'
--- openlp/core/ui/media/mediacontroller.py	2016-04-23 13:46:59 +0000
+++ openlp/core/ui/media/mediacontroller.py	2016-06-06 16:44:43 +0000
@@ -38,7 +38,6 @@
 from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\
     parse_optical_path
 from openlp.core.ui.lib.toolbar import OpenLPToolbar
-from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
 
 log = logging.getLogger(__name__)
 

=== modified file 'openlp/plugins/remotes/html/js/main.js'
--- openlp/plugins/remotes/html/js/main.js	2016-03-19 18:49:55 +0000
+++ openlp/plugins/remotes/html/js/main.js	2016-06-06 16:44:43 +0000
@@ -28,18 +28,23 @@
     );
   },
   pollServer: function () {
-    $.getJSON(
-      "/main/poll",
-      function (data, status) {
-        if (OpenLP.slideCount != data.results.slide_count) {
-          OpenLP.slideCount = data.results.slide_count;
-          OpenLP.loadSlide();
+    if ("WebSocket" in window) {
+        // Let us open a web socket
+        var ws = new WebSocket('ws://' + location.hostname + ':4318/main_poll');
+        ws.binaryType = 'arraybuffer';
+        ws.onmessage = function (evt) {
+            var msg = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(evt.data)));
+            if (OpenLP.slideCount != msg.results.slide_count) {
+              OpenLP.slideCount = msg.results.slide_count;
+              OpenLP.loadSlide();
+            }
         }
-      }
-    );
+    } else {
+        // The browser doesn't support WebSocket
+        alert("WebSocket NOT supported by your Browser!");
+    }      
   }
-}
+};
 $.ajaxSetup({ cache: false });
-setInterval("OpenLP.pollServer();", 500);
 OpenLP.pollServer();
 

=== modified file 'openlp/plugins/remotes/html/js/openlp.js'
--- openlp/plugins/remotes/html/js/openlp.js	2016-03-19 18:49:55 +0000
+++ openlp/plugins/remotes/html/js/openlp.js	2016-06-06 16:44:43 +0000
@@ -159,15 +159,19 @@
     );
   },
   pollServer: function () {
-    $.getJSON(
-      "/api/poll",
-      function (data, status) {
-        var prevItem = OpenLP.currentItem;
-        OpenLP.currentSlide = data.results.slide;
-        OpenLP.currentItem = data.results.item;
-        OpenLP.isSecure = data.results.isSecure;
-        OpenLP.isAuthorised = data.results.isAuthorised;
-        if ($("#service-manager").is(":visible")) {
+    if ("WebSocket" in window) {
+        // Let us open a web socket
+        var ws = new WebSocket('ws://' + location.hostname + ':4318/poll');
+        ws.binaryType = 'arraybuffer';
+        ws.onmessage = function (evt) {
+          var data = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(evt.data)));
+
+          var prevItem = OpenLP.currentItem;
+          OpenLP.currentSlide = data.results.slide;
+          OpenLP.currentItem = data.results.item;
+          OpenLP.isSecure = data.results.isSecure;
+          OpenLP.isAuthorised = data.results.isAuthorised;
+          if ($("#service-manager").is(":visible")) {
           if (OpenLP.currentService != data.results.service) {
             OpenLP.currentService = data.results.service;
             OpenLP.loadService();
@@ -185,7 +189,7 @@
           });
           $("#service-manager div[data-role=content] ul[data-role=listview]").listview("refresh");
         }
-        if ($("#slide-controller").is(":visible")) {
+          if ($("#slide-controller").is(":visible")) {
           if (prevItem != OpenLP.currentItem) {
             OpenLP.loadController();
             return;
@@ -205,8 +209,11 @@
           });
           $("#slide-controller div[data-role=content] ul[data-role=listview]").listview("refresh");
         }
-      }
-    );
+        }
+    } else {
+      // The browser doesn't support WebSocket
+      alert("WebSocket NOT supported by your Browser!");
+    }
   },
   nextItem: function (event) {
     event.preventDefault();
@@ -380,5 +387,4 @@
 $("#search").live("pageinit", function (event) {
   OpenLP.getSearchablePlugins();
 });
-setInterval("OpenLP.pollServer();", 500);
 OpenLP.pollServer();

=== modified file 'openlp/plugins/remotes/html/js/stage.js'
--- openlp/plugins/remotes/html/js/stage.js	2016-03-19 18:49:55 +0000
+++ openlp/plugins/remotes/html/js/stage.js	2016-06-06 16:44:43 +0000
@@ -135,11 +135,11 @@
       $("#nextslide").html(text);
     }
   },
-  updateClock: function(data) {
+  updateClock: function() {
     var div = $("#clock");
     var t = new Date();
     var h = t.getHours();
-    if (data.results.twelve && h > 12)
+    if (OpenLP.twelve && h > 12)
       h = h - 12;
     var m = t.getMinutes();
     if (m < 10)
@@ -147,24 +147,32 @@
     div.html(h + ":" + m);
   },
   pollServer: function () {
-    $.getJSON(
-      "/api/poll",
-      function (data, status) {
-        OpenLP.updateClock(data);
-        if (OpenLP.currentItem != data.results.item ||
-            OpenLP.currentService != data.results.service) {
-          OpenLP.currentItem = data.results.item;
-          OpenLP.currentService = data.results.service;
-          OpenLP.loadSlides();
-        }
-        else if (OpenLP.currentSlide != data.results.slide) {
-          OpenLP.currentSlide = parseInt(data.results.slide, 10);
-          OpenLP.updateSlide();
-        }
-      }
-    );
-  }
-}
+    if ("WebSocket" in window) {
+        // Let us open a web socket
+        var ws = new WebSocket('ws://' + location.hostname + ':4318/poll');
+        ws.binaryType = 'arraybuffer';
+        ws.onmessage = function (evt) {
+            var msg = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(evt.data)));
+            OpenLP.twelve = msg.results.twelve;
+            OpenLP.updateClock();
+            if (OpenLP.currentItem != msg.results.item ||
+                OpenLP.currentService != msg.results.service) {
+                OpenLP.currentItem = msg.results.item;
+                OpenLP.currentService = msg.results.service;
+                OpenLP.loadSlides();
+            }
+            else if (OpenLP.currentSlide != msg.results.slide) {
+                OpenLP.currentSlide = parseInt(msg.results.slide, 10);
+                OpenLP.updateSlide();
+            }
+        }
+    } else {
+        // The browser doesn't support WebSocket
+        alert("WebSocket NOT supported by your Browser!");
+    }
+  },
+};
 $.ajaxSetup({ cache: false });
-setInterval("OpenLP.pollServer();", 500);
+setInterval("OpenLP.updateClock();", 1000);
 OpenLP.pollServer();
+OpenLP.updateClock();
\ No newline at end of file

=== modified file 'openlp/plugins/remotes/lib/__init__.py'
--- openlp/plugins/remotes/lib/__init__.py	2015-12-31 22:46:06 +0000
+++ openlp/plugins/remotes/lib/__init__.py	2016-06-06 16:44:43 +0000
@@ -20,8 +20,8 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
-from .remotetab import RemoteTab
 from .httprouter import HttpRouter
 from .httpserver import OpenLPServer
+from .remotetab import RemoteTab
 
 __all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter']

=== modified file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py	2016-05-27 08:13:14 +0000
+++ openlp/plugins/remotes/lib/httprouter.py	2016-06-06 16:44:43 +0000
@@ -116,7 +116,7 @@
 
 from mako.template import Template
 
-from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, UiStrings
+from openlp.core.common import OpenLPMixin, RegistryProperties, AppLocation, Settings, Registry, translate, UiStrings
 from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities, create_thumb
 
 log = logging.getLogger(__name__)
@@ -131,7 +131,7 @@
 }
 
 
-class HttpRouter(RegistryProperties):
+class HttpRouter(RegistryProperties, OpenLPMixin):
     """
     This code is called by the HttpServer upon a request and it processes it based on the routing table.
     This code is stateless and is created on each request.
@@ -143,6 +143,7 @@
         """
         auth_code = "{user}:{password}".format(user=Settings().value('remotes/user id'),
                                                password=Settings().value('remotes/password'))
+        self.openlppoll = Registry().get('OpenLPPoll')
         try:
             self.auth = base64.b64encode(auth_code)
         except TypeError:
@@ -154,8 +155,8 @@
             ('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
             ('^/(main)$', {'function': self.serve_file, 'secure': False}),
             (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
-            (r'^/api/poll$', {'function': self.poll, 'secure': False}),
-            (r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
+            (r'^/api/poll$', {'function': self.openlppoll.poll, 'secure': False}),
+            (r'^/main/poll$', {'function': self.openlppoll.main_poll, 'secure': False}),
             (r'^/main/image$', {'function': self.main_image, 'secure': False}),
             (r'^/api/controller/(live|preview)/text$', {'function': self.controller_text, 'secure': False}),
             (r'^/api/controller/(live|preview)/(.*)$', {'function': self.controller, 'secure': True}),
@@ -468,35 +469,6 @@
         self.end_headers()
         return content
 
-    def poll(self):
-        """
-        Poll OpenLP to determine the current slide number and item name.
-        """
-        result = {
-            'service': self.service_manager.service_id,
-            'slide': self.live_controller.selected_row or 0,
-            'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
-            'twelve': Settings().value('remotes/twelve hour'),
-            'blank': self.live_controller.blank_screen.isChecked(),
-            'theme': self.live_controller.theme_screen.isChecked(),
-            'display': self.live_controller.desktop_screen.isChecked(),
-            'version': 2,
-            'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
-            'isAuthorised': self.authorised
-        }
-        self.do_json_header()
-        return json.dumps({'results': result}).encode()
-
-    def main_poll(self):
-        """
-        Poll OpenLP to determine the current slide count.
-        """
-        result = {
-            'slide_count': self.live_controller.slide_count
-        }
-        self.do_json_header()
-        return json.dumps({'results': result}).encode()
-
     def main_image(self):
         """
         Return the latest display image as a byte stream.

=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py	2016-05-27 08:13:14 +0000
+++ openlp/plugins/remotes/lib/httpserver.py	2016-06-06 16:44:43 +0000
@@ -25,25 +25,22 @@
 with OpenLP. It uses JSON to communicate with the remotes.
 """
 
-import ssl
-import socket
-import os
 import logging
 import time
 
 from PyQt5 import QtCore
 
-from openlp.core.common import AppLocation, Settings, RegistryProperties
+from openlp.core.common import Settings, RegistryProperties, OpenLPMixin
 
 from openlp.plugins.remotes.lib import HttpRouter
 
-from socketserver import BaseServer, ThreadingMixIn
+from socketserver import ThreadingMixIn
 from http.server import BaseHTTPRequestHandler, HTTPServer
 
 log = logging.getLogger(__name__)
 
 
-class CustomHandler(BaseHTTPRequestHandler, HttpRouter):
+class WebHandler(BaseHTTPRequestHandler, HttpRouter):
     """
     Stateless session handler to handle the HTTP request and process it.
     This class handles just the overrides to the base methods and the logic to invoke the methods within the HttpRouter
@@ -88,18 +85,20 @@
         self.http_server.start_server()
 
     def stop(self):
-        log.debug("stop called")
         self.http_server.stop = True
 
 
-class OpenLPServer(RegistryProperties):
-    def __init__(self):
+class OpenLPServer(RegistryProperties, OpenLPMixin):
+    """
+    Wrapper round a server instance
+    """
+    def __init__(self, secure=False):
         """
         Initialise the http server, and start the server of the correct type http / https
         """
         super(OpenLPServer, self).__init__()
-        log.debug('Initialise OpenLP')
         self.settings_section = 'remotes'
+        self.secure = secure
         self.http_thread = HttpThread(self)
         self.http_thread.start()
 
@@ -108,21 +107,14 @@
         Start the correct server and save the handler
         """
         address = Settings().value(self.settings_section + '/ip address')
-        self.address = address
-        self.is_secure = Settings().value(self.settings_section + '/https enabled')
-        self.needs_authentication = Settings().value(self.settings_section + '/authentication enabled')
-        if self.is_secure:
-            port = Settings().value(self.settings_section + '/https port')
-            self.port = port
-            self.start_server_instance(address, port, HTTPSServer)
-        else:
-            port = Settings().value(self.settings_section + '/port')
-            self.port = port
-            self.start_server_instance(address, port, ThreadingHTTPServer)
+        # Try to start secure server but not enabled.
+        port = Settings().value(self.settings_section + '/port')
+        self.start_server_instance(address, port, ThreadingHTTPServer)
+        # If HTTP server start listening
         if hasattr(self, 'httpd') and self.httpd:
             self.httpd.serve_forever()
         else:
-            log.debug('Failed to start server')
+            log.debug('Failed to start http server on port {port}'.format(port=port))
 
     def start_server_instance(self, address, port, server_class):
         """
@@ -135,7 +127,7 @@
         loop = 1
         while loop < 4:
             try:
-                self.httpd = server_class((address, port), CustomHandler)
+                self.httpd = server_class((address, port), WebHandler)
                 log.debug("Server started for class {name} {address} {port:d}".format(name=server_class,
                                                                                       address=address,
                                                                                       port=port))
@@ -145,8 +137,8 @@
                           "{loop:d} {running}".format(loop=loop, running=self.http_thread.isRunning()))
                 loop += 1
                 time.sleep(0.1)
-            except:
-                log.error('Failed to start server ')
+            except Exception as e:
+                log.error('Failed to start http server {why}'.format(why=e))
                 loop += 1
                 time.sleep(0.1)
 
@@ -158,19 +150,3 @@
             self.http_thread.stop()
         self.httpd = None
         log.debug('Stopped the server.')
-
-
-class HTTPSServer(HTTPServer):
-    def __init__(self, address, handler):
-        """
-        Initialise the secure handlers for the SSL server if required.s
-        """
-        BaseServer.__init__(self, address, handler)
-        local_data = AppLocation.get_directory(AppLocation.DataDir)
-        self.socket = ssl.SSLSocket(
-            sock=socket.socket(self.address_family, self.socket_type),
-            certfile=os.path.join(local_data, 'remotes', 'openlp.crt'),
-            keyfile=os.path.join(local_data, 'remotes', 'openlp.key'),
-            server_side=True)
-        self.server_bind()
-        self.server_activate()

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2016-05-27 08:13:14 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2016-06-06 16:44:43 +0000
@@ -88,42 +88,6 @@
         self.live_url.setOpenExternalLinks(True)
         self.http_setting_layout.addRow(self.live_url_label, self.live_url)
         self.left_layout.addWidget(self.http_settings_group_box)
-        self.https_settings_group_box = QtWidgets.QGroupBox(self.left_column)
-        self.https_settings_group_box.setCheckable(True)
-        self.https_settings_group_box.setChecked(False)
-        self.https_settings_group_box.setObjectName('https_settings_group_box')
-        self.https_settings_layout = QtWidgets.QFormLayout(self.https_settings_group_box)
-        self.https_settings_layout.setObjectName('https_settings_layout')
-        self.https_error_label = QtWidgets.QLabel(self.https_settings_group_box)
-        self.https_error_label.setVisible(False)
-        self.https_error_label.setWordWrap(True)
-        self.https_error_label.setObjectName('https_error_label')
-        self.https_settings_layout.addRow(self.https_error_label)
-        self.https_port_label = QtWidgets.QLabel(self.https_settings_group_box)
-        self.https_port_label.setObjectName('https_port_label')
-        self.https_port_spin_box = QtWidgets.QSpinBox(self.https_settings_group_box)
-        self.https_port_spin_box.setMaximum(32767)
-        self.https_port_spin_box.setObjectName('https_port_spin_box')
-        self.https_settings_layout.addRow(self.https_port_label, self.https_port_spin_box)
-        self.remote_https_url = QtWidgets.QLabel(self.https_settings_group_box)
-        self.remote_https_url.setObjectName('remote_http_url')
-        self.remote_https_url.setOpenExternalLinks(True)
-        self.remote_https_url_label = QtWidgets.QLabel(self.https_settings_group_box)
-        self.remote_https_url_label.setObjectName('remote_http_url_label')
-        self.https_settings_layout.addRow(self.remote_https_url_label, self.remote_https_url)
-        self.stage_https_url_label = QtWidgets.QLabel(self.http_settings_group_box)
-        self.stage_https_url_label.setObjectName('stage_https_url_label')
-        self.stage_https_url = QtWidgets.QLabel(self.https_settings_group_box)
-        self.stage_https_url.setObjectName('stage_https_url')
-        self.stage_https_url.setOpenExternalLinks(True)
-        self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
-        self.live_https_url_label = QtWidgets.QLabel(self.https_settings_group_box)
-        self.live_https_url_label.setObjectName('live_url_label')
-        self.live_https_url = QtWidgets.QLabel(self.https_settings_group_box)
-        self.live_https_url.setObjectName('live_https_url')
-        self.live_https_url.setOpenExternalLinks(True)
-        self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url)
-        self.left_layout.addWidget(self.https_settings_group_box)
         self.user_login_group_box = QtWidgets.QGroupBox(self.left_column)
         self.user_login_group_box.setCheckable(True)
         self.user_login_group_box.setChecked(False)
@@ -177,8 +141,6 @@
         self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
         self.address_edit.textChanged.connect(self.set_urls)
         self.port_spin_box.valueChanged.connect(self.set_urls)
-        self.https_port_spin_box.valueChanged.connect(self.set_urls)
-        self.https_settings_group_box.clicked.connect(self.https_changed)
 
     def retranslateUi(self):
         self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
@@ -200,14 +162,6 @@
             translate('RemotePlugin.RemoteTab',
                       'Scan the QR code or click <a href="{qr}">download</a> to install the iOS app from the App '
                       'Store.').format(qr='https://itunes.apple.com/app/id1096218725'))
-        self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
-        self.https_error_label.setText(
-            translate('RemotePlugin.RemoteTab', 'Could not find an SSL certificate. The HTTPS server will not be '
-                      'available unless an SSL certificate is found. Please see the manual for more information.'))
-        self.https_port_label.setText(self.port_label.text())
-        self.remote_https_url_label.setText(self.remote_url_label.text())
-        self.stage_https_url_label.setText(self.stage_url_label.text())
-        self.live_https_url_label.setText(self.live_url_label.text())
         self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
         self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
         self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
@@ -218,17 +172,11 @@
         """
         ip_address = self.get_ip_address(self.address_edit.text())
         http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.value())
-        https_url = 'https://{url}:{text}/'.format(url=ip_address, text=self.https_port_spin_box.value())
         self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url))
-        self.remote_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url))
         http_url_temp = http_url + 'stage'
-        https_url_temp = https_url + 'stage'
         self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
-        self.stage_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url_temp))
         http_url_temp = http_url + 'main'
-        https_url_temp = https_url + 'main'
         self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
-        self.live_https_url.setText('<a href="{url}">{url}</a>'.format(url=https_url_temp))
 
     def get_ip_address(self, ip_address):
         """
@@ -254,43 +202,25 @@
         """
         Load the configuration and update the server configuration if necessary
         """
-        self.is_secure = Settings().value(self.settings_section + '/https enabled')
         self.port_spin_box.setValue(Settings().value(self.settings_section + '/port'))
-        self.https_port_spin_box.setValue(Settings().value(self.settings_section + '/https port'))
         self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
         self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
         self.twelve_hour_check_box.setChecked(self.twelve_hour)
         self.thumbnails = Settings().value(self.settings_section + '/thumbnails')
         self.thumbnails_check_box.setChecked(self.thumbnails)
-        local_data = AppLocation.get_directory(AppLocation.DataDir)
-        if not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.crt')) or \
-                not os.path.exists(os.path.join(local_data, 'remotes', 'openlp.key')):
-            self.https_settings_group_box.setChecked(False)
-            self.https_settings_group_box.setEnabled(False)
-            self.https_error_label.setVisible(True)
-        else:
-            self.https_settings_group_box.setChecked(Settings().value(self.settings_section + '/https enabled'))
-            self.https_settings_group_box.setEnabled(True)
-            self.https_error_label.setVisible(False)
         self.user_login_group_box.setChecked(Settings().value(self.settings_section + '/authentication enabled'))
         self.user_id.setText(Settings().value(self.settings_section + '/user id'))
         self.password.setText(Settings().value(self.settings_section + '/password'))
         self.set_urls()
-        self.https_changed()
 
     def save(self):
         """
         Save the configuration and update the server configuration if necessary
         """
         if Settings().value(self.settings_section + '/ip address') != self.address_edit.text() or \
-                Settings().value(self.settings_section + '/port') != self.port_spin_box.value() or \
-                Settings().value(self.settings_section + '/https port') != self.https_port_spin_box.value() or \
-                Settings().value(self.settings_section + '/https enabled') != \
-                self.https_settings_group_box.isChecked():
+                Settings().value(self.settings_section + '/port') != self.port_spin_box.value():
             self.settings_form.register_post_process('remotes_config_updated')
         Settings().setValue(self.settings_section + '/port', self.port_spin_box.value())
-        Settings().setValue(self.settings_section + '/https port', self.https_port_spin_box.value())
-        Settings().setValue(self.settings_section + '/https enabled', self.https_settings_group_box.isChecked())
         Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
         Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
         Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
@@ -317,12 +247,6 @@
         if check_state == QtCore.Qt.Checked:
             self.thumbnails = True
 
-    def https_changed(self):
-        """
-        Invert the HTTP group box based on Https group settings
-        """
-        self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())
-
     def generate_icon(self):
         """
         Generate icon for main window
@@ -330,12 +254,6 @@
         self.remote_server_icon.hide()
         icon = QtGui.QImage(':/remote/network_server.png')
         icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
-        if self.is_secure:
-            overlay = QtGui.QImage(':/remote/network_ssl.png')
-            overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
-            painter = QtGui.QPainter(icon)
-            painter.drawImage(0, 0, overlay)
-            painter.end()
         if Settings().value(self.settings_section + '/authentication enabled'):
             overlay = QtGui.QImage(':/remote/network_auth.png')
             overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)

=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py	2016-01-04 00:18:01 +0000
+++ openlp/plugins/remotes/remoteplugin.py	2016-06-06 16:44:43 +0000
@@ -24,6 +24,7 @@
 
 from PyQt5 import QtWidgets
 
+from openlp.core.common import OpenLPMixin
 from openlp.core.lib import Plugin, StringContent, translate, build_icon
 from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
 
@@ -32,8 +33,7 @@
 __default_settings__ = {
     'remotes/twelve hour': True,
     'remotes/port': 4316,
-    'remotes/https port': 4317,
-    'remotes/https enabled': False,
+    'remotes/websocket port': 4318,
     'remotes/user id': 'openlp',
     'remotes/password': 'password',
     'remotes/authentication enabled': False,
@@ -42,7 +42,7 @@
 }
 
 
-class RemotesPlugin(Plugin):
+class RemotesPlugin(Plugin, OpenLPMixin):
     log.info('Remote Plugin loaded')
 
     def __init__(self):
@@ -59,7 +59,6 @@
         """
         Initialise the remotes plugin, and start the http server
         """
-        log.debug('initialise')
         super(RemotesPlugin, self).initialise()
         self.server = OpenLPServer()
         if not hasattr(self, 'remote_server_icon'):
@@ -82,7 +81,6 @@
         """
         Tidy up and close down the http server
         """
-        log.debug('finalise')
         super(RemotesPlugin, self).finalise()
         if self.server:
             self.server.stop_server()

=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py	2016-05-17 21:28:27 +0000
+++ scripts/check_dependencies.py	2016-06-06 16:44:43 +0000
@@ -93,6 +93,9 @@
     'bs4',
     'mako',
     'uno',
+    'websockets',
+    'asyncio',
+    'waitress',
     'six'
 ]
 

=== modified file 'scripts/jenkins_script.py'
--- scripts/jenkins_script.py	2015-12-31 22:46:06 +0000
+++ scripts/jenkins_script.py	2016-06-06 16:44:43 +0000
@@ -171,7 +171,7 @@
     # Determine the branch's name
     repo_name = ''
     for line in output_list:
-        # Check if it is remote branch.
+        # Check if it is api branch.
         if 'push branch' in line:
             match = re.match(REPO_REGEX, line)
             if match:

=== added file 'scripts/websocket_client.py'
--- scripts/websocket_client.py	1970-01-01 00:00:00 +0000
+++ scripts/websocket_client.py	2016-06-06 16:44:43 +0000
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 OpenLP Developers                                   #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+
+import asyncio
+import websockets
+import random
+
+async def tester():
+    async with websockets.connect('ws://localhost:4318/poll') as websocket:
+
+        while True:
+            greeting = await websocket.recv()
+            print("< {}".format(greeting))
+            import time
+            time.sleep(random.random() * 3)
+
+asyncio.get_event_loop().run_until_complete(tester())

=== modified file 'tests/functional/openlp_core_ui/test_mainwindow.py'
--- tests/functional/openlp_core_ui/test_mainwindow.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_core_ui/test_mainwindow.py	2016-06-06 16:44:43 +0000
@@ -149,7 +149,7 @@
         # WHEN: you check the started functions
 
         # THEN: the following registry functions should have been registered
-        self.assertEqual(len(self.registry.service_list), 6, 'The registry should have 6 services.')
+        self.assertEqual(len(self.registry.service_list), 7, 'The registry should have 7 services.')
         self.assertEqual(len(self.registry.functions_list), 17, 'The registry should have 17 functions')
         self.assertTrue('application' in self.registry.service_list, 'The application should have been registered.')
         self.assertTrue('main_window' in self.registry.service_list, 'The main_window should have been registered.')

=== modified file 'tests/functional/openlp_plugins/remotes/test_remotetab.py'
--- tests/functional/openlp_plugins/remotes/test_remotetab.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_plugins/remotes/test_remotetab.py	2016-06-06 16:44:43 +0000
@@ -37,8 +37,6 @@
 __default_settings__ = {
     'remotes/twelve hour': True,
     'remotes/port': 4316,
-    'remotes/https port': 4317,
-    'remotes/https enabled': False,
     'remotes/user id': 'openlp',
     'remotes/password': 'password',
     'remotes/authentication enabled': False,
@@ -114,12 +112,9 @@
             self.form.set_urls()
             # THEN: the following screen values should be set
             self.assertEqual(self.form.address_edit.text(), ZERO_URL, 'The default URL should be set on the screen')
-            self.assertEqual(self.form.https_settings_group_box.isEnabled(), False,
-                             'The Https box should not be enabled')
-            self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
-                             'The Https checked box should note be Checked')
             self.assertEqual(self.form.user_login_group_box.isChecked(), False,
                              'The authentication box should not be enabled')
+<<<<<<< TREE
 
     def test_set_certificate_urls(self):
         """
@@ -147,3 +142,5 @@
                              'The Https checked box should be Checked')
             self.assertEqual(self.form.https_settings_group_box.isEnabled(), True,
                              'The Https box should be enabled')
+=======
+>>>>>>> MERGE-SOURCE

=== modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py	2016-05-31 21:40:13 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py	2016-06-06 16:44:43 +0000
@@ -28,6 +28,7 @@
 
 from openlp.core.common import Settings, Registry
 from openlp.core.ui import ServiceManager
+from openlp.core.lib.api import OpenLPPoll
 from openlp.plugins.remotes.lib.httpserver import HttpRouter
 from tests.functional import MagicMock, patch, mock_open
 from tests.helpers.testmixin import TestMixin
@@ -35,8 +36,6 @@
 __default_settings__ = {
     'remotes/twelve hour': True,
     'remotes/port': 4316,
-    'remotes/https port': 4317,
-    'remotes/https enabled': False,
     'remotes/user id': 'openlp',
     'remotes/password': 'password',
     'remotes/authentication enabled': False,
@@ -74,6 +73,8 @@
         # GIVEN: A default configuration
         Settings().setValue('remotes/user id', 'openlp')
         Settings().setValue('remotes/password', 'password')
+        poll = MagicMock()
+        Registry().register('OpenLPPoll', poll)
 
         # WHEN: called with the defined userid
         router = HttpRouter()
@@ -157,15 +158,13 @@
         """
         # GIVEN: a defined router with two slides
         Registry.create()
-        Registry().register('live_controller', MagicMock)
-        router = HttpRouter()
-        router.send_response = MagicMock()
-        router.send_header = MagicMock()
-        router.end_headers = MagicMock()
-        router.live_controller.slide_count = 2
+        live_controller = MagicMock()
+        Registry().register('live_controller', live_controller)
+        poll = OpenLPPoll()
+        live_controller.slide_count = 2
 
         # WHEN: main poll called
-        results = router.main_poll()
+        results = poll.main_poll()
 
         # THEN: the correct response should be returned
         self.assertEqual(results.decode('utf-8'), '{"results": {"slide_count": 2}}',
@@ -328,7 +327,7 @@
 
     def test_remote_next(self):
         """
-        Test service manager receives remote next click properly (bug 1407445)
+        Test service manager receives api next click properly (bug 1407445)
         """
         # GIVEN: initial setup and mocks
         self.router.routes = [(r'^/api/service/(.*)$', {'function': self.router.service, 'secure': False})]
@@ -349,7 +348,7 @@
 
     def test_remote_previous(self):
         """
-        Test service manager receives remote previous click properly (bug 1407445)
+        Test service manager receives api previous click properly (bug 1407445)
         """
         # GIVEN: initial setup and mocks
         self.router.routes = [(r'^/api/service/(.*)$', {'function': self.router.service, 'secure': False})]


Follow ups