def test_pack_unpack_presentationStatus(self): STATUSES=[ ( ["okay"], "okay" ), ( ["transitioning"], "transitioning" ), ( ["fault"], "fault" ), ( ["other"], "other" ), ( ["okay", "sub"], "okay sub" ), ( ["transitioning", "sub1", "sub2"], "transitioning sub1 sub2" ), ] for (VALUE, ENCODED) in STATUSES: c=CII(presentationStatus=VALUE) self.assertEquals(c.presentationStatus, VALUE) msg=c.pack() j=json.loads(msg) self.assertEquals(j["presentationStatus"], ENCODED) self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals(OMIT, d.protocolVersion) self.assertEquals(OMIT, d.contentId) self.assertEquals(OMIT, d.contentIdStatus) self.assertEquals(VALUE, d.presentationStatus) self.assertEquals(OMIT, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(OMIT, d.teUrl) self.assertEquals(OMIT, d.timelines) self.assertEquals(OMIT, d.private)
def onClientConnect(self, webSock): """Force not sending, if blocking""" if not self._blocking: super(BlockableCIIServer,self).onClientConnect(webSock) else: self.getConnections()[webSock]["prevCII"] = CII() self.onNumClientsChange(len(self.getConnections()))
def _onCII(self, newCII): self.latestCII = newCII self.onCiiReceived(newCII) # take a diff since we cannot assume the received message is a diff diff=CII.diff(self.cii, newCII) changes=diff.definedProperties() if len(changes) > 0: self.log.debug("Changed properties: "+ " ".join(changes)) self.cii.update(diff) # now we examine changes and fire change specific callbacks as well as a general callback for name in changes: if name in changes: funcname = self._callBackFuncNames[name] callback = getattr(self, funcname) if callback is not None: newValue=getattr(diff, name) callback(newValue) # fire general catch-all callback self.onChange(changes) else: self.log.debug("No properties have changed")
def _onCII(self, newCII): self.latestCII = newCII self.onCiiReceived(newCII) # take a diff since we cannot assume the received message is a diff diff = CII.diff(self.cii, newCII) changes = diff.definedProperties() if len(changes) > 0: self.log.debug("Changed properties: " + " ".join(changes)) self.cii.update(diff) # now we examine changes and fire change specific callbacks as well as a general callback for name in changes: if name in changes: funcname = self._callBackFuncNames[name] callback = getattr(self, funcname) if callback is not None: newValue = getattr(diff, name) callback(newValue) # fire general catch-all callback self.onChange(changes) else: self.log.debug("No properties have changed")
def _ws_on_message(self, msg): self.log.debug("Message received.") if not msg.is_text: self._ws_on_error("Protocol error - message received was not a text frame") return try: cii = CII.unpack(msg.data) except Exception,e: self._ws_on_error("Protocol error - message received could not be parsed as a CII message: "+str(msg)+". Continuing anyway. Cause was: "+str(e)+"\n") return
def test_pack_unpack_protocolVersion(self): c=CII(protocolVersion="1.1") self.assertEquals(c.protocolVersion, "1.1") msg=c.pack() j=json.loads(msg) self.assertEquals(j["protocolVersion"], "1.1") self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals("1.1", c.protocolVersion) self.assertEquals(OMIT, d.contentId) self.assertEquals(OMIT, d.contentIdStatus) self.assertEquals(OMIT, d.presentationStatus) self.assertEquals(OMIT, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(OMIT, d.teUrl) self.assertEquals(OMIT, d.timelines) self.assertEquals(OMIT, d.private)
def test_create_empty(self): c=CII() self.assertEquals(OMIT, c.protocolVersion) self.assertEquals(OMIT, c.contentId) self.assertEquals(OMIT, c.contentIdStatus) self.assertEquals(OMIT, c.presentationStatus) self.assertEquals(OMIT, c.mrsUrl) self.assertEquals(OMIT, c.wcUrl) self.assertEquals(OMIT, c.tsUrl) self.assertEquals(OMIT, c.teUrl) self.assertEquals(OMIT, c.timelines) self.assertEquals(OMIT, c.private)
def test_pack_unpack_teUrl(self): VALUE="ws://1.2.3.4:5678/seilgr" c=CII(teUrl=VALUE) self.assertEquals(c.teUrl, VALUE) msg=c.pack() j=json.loads(msg) self.assertEquals(j["teUrl"], VALUE) self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals(OMIT, d.protocolVersion) self.assertEquals(OMIT, d.contentId) self.assertEquals(OMIT, d.contentIdStatus) self.assertEquals(OMIT, d.presentationStatus) self.assertEquals(OMIT, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(VALUE, d.teUrl) self.assertEquals(OMIT, d.timelines) self.assertEquals(OMIT, d.private)
def test_pack_unpack_mrsUrl(self): VALUE="http://blah.com" c=CII(mrsUrl=VALUE) self.assertEquals(c.mrsUrl, VALUE) msg=c.pack() j=json.loads(msg) self.assertEquals(j["mrsUrl"], VALUE) self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals(OMIT, d.protocolVersion) self.assertEquals(OMIT, d.contentId) self.assertEquals(OMIT, d.contentIdStatus) self.assertEquals(OMIT, d.presentationStatus) self.assertEquals(VALUE, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(OMIT, d.teUrl) self.assertEquals(OMIT, d.timelines) self.assertEquals(OMIT, d.private)
def test_pack_unpack_contentIdStatus(self): for VALUE in ["partial","final"]: c=CII(contentIdStatus=VALUE) self.assertEquals(c.contentIdStatus, VALUE) msg=c.pack() j=json.loads(msg) self.assertEquals(j["contentIdStatus"], VALUE) self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals(OMIT, d.protocolVersion) self.assertEquals(OMIT, d.contentId) self.assertEquals(VALUE, d.contentIdStatus) self.assertEquals(OMIT, d.presentationStatus) self.assertEquals(OMIT, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(OMIT, d.teUrl) self.assertEquals(OMIT, d.timelines) self.assertEquals(OMIT, d.private)
def test_pack_unpack_contentId(self): VALUE="dvb://a.b.c.d" c=CII(contentId=VALUE) self.assertEquals(c.contentId, VALUE) msg=c.pack() j=json.loads(msg) self.assertEquals(j["contentId"], VALUE) self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals(OMIT, d.protocolVersion) self.assertEquals(VALUE, d.contentId) self.assertEquals(OMIT, d.contentIdStatus) self.assertEquals(OMIT, d.presentationStatus) self.assertEquals(OMIT, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(OMIT, d.teUrl) self.assertEquals(OMIT, d.timelines) self.assertEquals(OMIT, d.private)
def _onClientMessage(self, webSock, message): msg = json.loads(str(message)) print msg print if "cii" in msg: cii = CII.unpack(json.dumps(msg["cii"])) else: cii = CII() controlTimestamps = {} if "controlTimestamps" in msg: for timelineSelector, recvControlTimestamp in msg["controlTimestamps"].items(): ct = ControlTimestamp.unpack(json.dumps(recvControlTimestamp)) controlTimestamps[timelineSelector] = ct options = {} if "options" in msg: options = msg["options"] self.onUpdate(cii,controlTimestamps, options)
def test_unpack_ignore_unknown_fields(self): msg="""{ "flurble" : 5 }""" c=CII.unpack(msg) self.assertEquals(OMIT, c.protocolVersion) self.assertEquals(OMIT, c.contentId) self.assertEquals(OMIT, c.contentIdStatus) self.assertEquals(OMIT, c.presentationStatus) self.assertEquals(OMIT, c.mrsUrl) self.assertEquals(OMIT, c.wcUrl) self.assertEquals(OMIT, c.tsUrl) self.assertEquals(OMIT, c.teUrl) self.assertEquals(OMIT, c.timelines) self.assertEquals(OMIT, c.private)
def test_unpack_empty_message(self): msg="{}" c=CII.unpack(msg) self.assertEquals(OMIT, c.protocolVersion) self.assertEquals(OMIT, c.contentId) self.assertEquals(OMIT, c.contentIdStatus) self.assertEquals(OMIT, c.presentationStatus) self.assertEquals(OMIT, c.mrsUrl) self.assertEquals(OMIT, c.wcUrl) self.assertEquals(OMIT, c.tsUrl) self.assertEquals(OMIT, c.teUrl) self.assertEquals(OMIT, c.timelines) self.assertEquals(OMIT, c.private)
def __init__(self, ciiUrl): """\ **Initialisation takes the following parameters:** :param ciiUrl: (:class:`str`) The WebSocket URL of the CSS-CII Server (e.g. "ws://127.0.0.1/myservice/cii") """ super(CIIClient,self).__init__() self.log = logging.getLogger("dvbcss.protocol.client.cii.CIIClient") self._conn = CIIClientConnection(ciiUrl) self._conn.onCII = self._onCII self._conn.onConnected = self._onConnectionOpen self._conn.onDisconnected = self._onConnectionClose self._conn.onProtocolError = self._onProtocolError self.connected = False #: True if currently connected to the server, otherwise False. self.cii = CII() #: (:class:`~dvbcss.protocol.cii.CII`) CII object representing the CII state at the server self.latestCII = None #: (:class:`~dvbcss.protocol.cii.CII` or :class:`None`) The most recent CII message received from the server or None if nothing has yet been received. self._callBackFuncNames = {} for name in CII.allProperties(): funcname = "on" + name[0].upper() + name[1:] + "Change" self._callBackFuncNames[name] = funcname
def _ws_on_message(self, msg): self.log.debug("Message received.") if not msg.is_text: self._ws_on_error( "Protocol error - message received was not a text frame") return try: cii = CII.unpack(msg.data) except Exception, e: self._ws_on_error( "Protocol error - message received could not be parsed as a CII message: " + str(msg) + ". Continuing anyway. Cause was: " + str(e) + "\n") return
def __init__(self, ciiUrl): """\ **Initialisation takes the following parameters:** :param ciiUrl: (:class:`str`) The WebSocket URL of the CSS-CII Server (e.g. "ws://127.0.0.1/myservice/cii") """ super(CIIClient, self).__init__() self.log = logging.getLogger("dvbcss.protocol.client.cii.CIIClient") self._conn = CIIClientConnection(ciiUrl) self._conn.onCII = self._onCII self._conn.onConnected = self._onConnectionOpen self._conn.onDisconnected = self._onConnectionClose self._conn.onProtocolError = self._onProtocolError self.connected = False #: True if currently connected to the server, otherwise False. self.cii = CII( ) #: (:class:`~dvbcss.protocol.cii.CII`) CII object representing the CII state at the server self.latestCII = None #: (:class:`~dvbcss.protocol.cii.CII` or :class:`None`) The most recent CII message received from the server or None if nothing has yet been received. self._callBackFuncNames = {} for name in CII.allProperties(): funcname = "on" + name[0].upper() + name[1:] + "Change" self._callBackFuncNames[name] = funcname
def updateClients(self, sendOnlyDiff=True, sendIfEmpty=False): """\ Send update of current CII state from the :data:`CIIServer.cii` object to all connected clients. :param sendOnlyDiff: (bool, default=True) Send only the properties in the CII state that have changed since last time a message was sent. Set to False to send the entire message. :param sendIfEmpty: (bool, default=False) Set to True to force that a CII message be sent, even if it will be empty (e.g. no change since last time) By default this method will only send a CII message to clients informing them of the differencesin state since last time a message was sent to them. If no properties have changed at all, then no message will be sent. The two optional arguments allow you to change this behaviour. For example, to force the messages sent to include all properties, even if they have not changed: .. code-block:: python myCiiServer.updateClients(sendOnlyDiff=False) To additionally force it to send even if the CII state held at this server has no values for any of the properties: .. code-block:: python myCiiServer.updateClients(sendOnlyDiff=False, sendIfEmpty=True) """ connections = self.getConnections() for webSock in connections: self.log.debug("Sending CII to connection " + webSock.id()) connectionData = connections[webSock] prevCII = connectionData["prevCII"] # perform rewrite substitutions, if any cii = self._customiseCii(webSock) # work out whether we are sending the full CII or a diff if sendOnlyDiff: diff = CII.diff(prevCII, cii) toSend = diff # enforce requirement that contentId must be accompanied by contentIdStatus if diff.contentId != OMIT: toSend.contentIdStatus = cii.contentIdStatus else: toSend = cii # only send if forced to, or if the mesage to send is not empty (all OMITs) if sendIfEmpty or toSend.definedProperties(): webSock.send(toSend.pack()) connectionData["prevCII"] = cii.copy()
def updateClients(self, sendOnlyDiff=True,sendIfEmpty=False): """\ Send update of current CII state from the :data:`CIIServer.cii` object to all connected clients. :param sendOnlyDiff: (bool, default=True) Send only the properties in the CII state that have changed since last time a message was sent. Set to False to send the entire message. :param sendIfEmpty: (bool, default=False) Set to True to force that a CII message be sent, even if it will be empty (e.g. no change since last time) By default this method will only send a CII message to clients informing them of the differencesin state since last time a message was sent to them. If no properties have changed at all, then no message will be sent. The two optional arguments allow you to change this behaviour. For example, to force the messages sent to include all properties, even if they have not changed: .. code-block:: python myCiiServer.updateClients(sendOnlyDiff=False) To additionally force it to send even if the CII state held at this server has no values for any of the properties: .. code-block:: python myCiiServer.updateClients(sendOnlyDiff=False, sendIfEmpty=True) """ connections = self.getConnections() for webSock in connections: self.log.debug("Sending CII to connection "+webSock.id()) connectionData = connections[webSock] prevCII = connectionData["prevCII"] # work out whether we are sending the full CII or a diff if sendOnlyDiff: diff = CII.diff(prevCII, self.cii) toSend = diff # enforce requirement that contentId must be accompanied by contentIdStatus if diff.contentId != OMIT: toSend.contentIdStatus = self.cii.contentIdStatus else: toSend = self.cii # only send if forced to, or if the mesage to send is not empty (all OMITs) if sendIfEmpty or toSend.definedProperties(): webSock.send(toSend.pack()) connectionData["prevCII"] = self.cii.copy()
def __init__(self, maxConnectionsAllowed=-1, enabled=True, initialCII=CII(protocolVersion="1.1"), rewriteHostPort=[]): """\ **Initialisation takes the following parameters:** :param maxConnectionsAllowed: (int, default=-1) Maximum number of concurrent connections to be allowed, or -1 to allow as many connections as resources allow. :param enabled: (bool, default=True) Whether the endpoint is initially enabled (True) or disabled (False) :param initialCII: (:class:`dvbcss.protocol.cii.CII`, default=CII(protocolVersion="1.1")) Initial value of CII state. :param rewriteHostPort: (list) List of CII property names for which the sub-string '{{host}}' '{{port}}' will be replaced with the host and port that the client connected to. """ super(CIIServer, self).__init__(maxConnectionsAllowed=maxConnectionsAllowed, enabled=enabled) self.cii = initialCII.copy() self._rewriteHostPort = rewriteHostPort[:] """\
def __init__(self, enabled=True, initialCII=CII(protocolVersion="1.1")): super(MockCiiServer, self).__init__() self.cii = initialCII.copy() self._connections = {} self._enabled = enabled self._updateClientsCalled = False
class CIIServer(WSServerBase): """\ The CIIServer class implements a server for the CSS-CII protocol. It transparently manages the connection and disconnection of clients and provides an interface for simply setting the CII state and requesting that it be pushed to any connected clients. Must be used in conjunction with a cherrypy web server: 1. Ensure the ws4py :class:`~ws4py.server.cherrypyserver.WebSocketPlugin` is subscribed, to the cherrypy server. E.g. .. code-block:: python WebSocketPlugin(cherrypy.engine).subscribe() 2. Mount the instance onto a particular URL path on a cherrypy web server. Set the config properties for the URL it is to be mounted at as follows: .. code-block:: python { 'tools.dvb_cii.on' : True, 'tools.dvb_cii.handler_cls': myCiiServerInstance.handler } Update the :data:`cii` property with the CII state information and call the :func:`updateClients` method to propagate state changes to any connected clients. When the server is "disabled" it will refuse attempts to connect by sending the HTTP status response 403 "Forbidden". When the server has reached its connection limit, it will refuse attempts to connect by sending the HTTP status response 503 "Service unavailable". This object provides properties: * :data:`enabled` (read/write) controls whether this server is enabled or not * :data:`cii` (read/write) the CII state that is being shared to connected clients To allow for servers serving multiple network interfaces, or where the IP address of the interface is not easy to determine, CII Server can be asked to automatically substitute the host and port with the one that the client connected to. Specify the list of property names for which this shoud happen as an optional `rewritheostPort` argument when intialising the CIIServer, then use `{{host}}` `{{port}}` within those properties. """ connectionIdPrefix = "cii" loggingName = "dvb-css.protocol.server.cii.CIIServer" getDefaultConnectionData = lambda self: { "prevCII": CII() } # default state for a new connection - no CII info transferred to client yet def __init__(self, maxConnectionsAllowed=-1, enabled=True, initialCII=CII(protocolVersion="1.1"), rewriteHostPort=[]): """\ **Initialisation takes the following parameters:** :param maxConnectionsAllowed: (int, default=-1) Maximum number of concurrent connections to be allowed, or -1 to allow as many connections as resources allow. :param enabled: (bool, default=True) Whether the endpoint is initially enabled (True) or disabled (False) :param initialCII: (:class:`dvbcss.protocol.cii.CII`, default=CII(protocolVersion="1.1")) Initial value of CII state. :param rewriteHostPort: (list) List of CII property names for which the sub-string '{{host}}' '{{port}}' will be replaced with the host and port that the client connected to. """ super(CIIServer, self).__init__(maxConnectionsAllowed=maxConnectionsAllowed, enabled=enabled) self.cii = initialCII.copy() self._rewriteHostPort = rewriteHostPort[:] """\ A :class:`dvbcss.protocol.cii.CII` message object representing current CII state. Set the attributes of this object to update that state. When :func:`updateClients` is called, it is this state that will be sent to connected clients. """ def _customiseCii(self, webSock): cii = self.cii.copy() host = webSock.local_address[0] port = str(webSock.local_address[1]) for propName in cii.definedProperties(): if propName in self._rewriteHostPort: propVal = getattr(cii, propName).replace("{{host}}", host).replace( "{{port}}", port) setattr(cii, propName, propVal) return cii def updateClients(self, sendOnlyDiff=True, sendIfEmpty=False): """\ Send update of current CII state from the :data:`CIIServer.cii` object to all connected clients. :param sendOnlyDiff: (bool, default=True) Send only the properties in the CII state that have changed since last time a message was sent. Set to False to send the entire message. :param sendIfEmpty: (bool, default=False) Set to True to force that a CII message be sent, even if it will be empty (e.g. no change since last time) By default this method will only send a CII message to clients informing them of the differencesin state since last time a message was sent to them. If no properties have changed at all, then no message will be sent. The two optional arguments allow you to change this behaviour. For example, to force the messages sent to include all properties, even if they have not changed: .. code-block:: python myCiiServer.updateClients(sendOnlyDiff=False) To additionally force it to send even if the CII state held at this server has no values for any of the properties: .. code-block:: python myCiiServer.updateClients(sendOnlyDiff=False, sendIfEmpty=True) """ connections = self.getConnections() for webSock in connections: self.log.debug("Sending CII to connection " + webSock.id()) connectionData = connections[webSock] prevCII = connectionData["prevCII"] # perform rewrite substitutions, if any cii = self._customiseCii(webSock) # work out whether we are sending the full CII or a diff if sendOnlyDiff: diff = CII.diff(prevCII, cii) toSend = diff # enforce requirement that contentId must be accompanied by contentIdStatus if diff.contentId != OMIT: toSend.contentIdStatus = cii.contentIdStatus else: toSend = cii # only send if forced to, or if the mesage to send is not empty (all OMITs) if sendIfEmpty or toSend.definedProperties(): webSock.send(toSend.pack()) connectionData["prevCII"] = cii.copy() def onClientConnect(self, webSock): """If you override this method you must call the base class implementation.""" self.log.info("Sending initial CII message for connection " + webSock.id()) cii = self._customiseCii(webSock) webSock.send(cii.pack()) self.getConnections()[webSock]["prevCII"] = cii.copy() def onClientDisconnect(self, webSock, connectionData): """If you override this method you must call the base class implementation.""" pass def onClientMessage(self, webSock, message): """If you override this method you must call the base class implementation.""" self.log.info("Received unexpected message on connection" + webSock.id() + " : " + str(message))
def test_pack_presentationStatus_not_a_string(self): c=CII(presentationStatus="okay") with self.assertRaises(ValueError): c.pack()
def test_pack_empty_message(self): c=CII() msg=c.pack() j=json.loads(msg) self.assertEquals(j,{})
class CIIClient(object): """\ Manages a CSS-CII protocol connection to a CSS-CII Server and notifies of changes to CII state. Use by subclassing and overriding the following methods: * :func:`onConnected` * :func:`onDisconnected` * :func:`onChange` * individual `onXXXXChange()` methods named after each CII property * :func:`onCiiReceived` (do not use, by preference) If you do not wish to subclass, you can instead create an instance of this class and replace the methods listed above with your own functions dynamically. The :func:`connect` and :func:`disconnect` methods connect and disconnect the connection to the server and :func:`getStatusSummary` provides a human readable summary of CII state. This object also provides properties you can query: * :data:`cii` represents the current state of CII at the server * :data:`latestCII` is the most recently CII message received from the server * :data:`connected` indicates whether the connection is currently connect """ def __init__(self, ciiUrl): """\ **Initialisation takes the following parameters:** :param ciiUrl: (:class:`str`) The WebSocket URL of the CSS-CII Server (e.g. "ws://127.0.0.1/myservice/cii") """ super(CIIClient,self).__init__() self.log = logging.getLogger("dvbcss.protocol.client.cii.CIIClient") self._conn = CIIClientConnection(ciiUrl) self._conn.onCII = self._onCII self._conn.onConnected = self._onConnectionOpen self._conn.onDisconnected = self._onConnectionClose self._conn.onProtocolError = self._onProtocolError self.connected = False #: True if currently connected to the server, otherwise False. self.cii = CII() #: (:class:`~dvbcss.protocol.cii.CII`) CII object representing the CII state at the server self.latestCII = None #: (:class:`~dvbcss.protocol.cii.CII` or :class:`None`) The most recent CII message received from the server or None if nothing has yet been received. self._callBackFuncNames = {} for name in CII.allProperties(): funcname = "on" + name[0].upper() + name[1:] + "Change" self._callBackFuncNames[name] = funcname def onConnected(self): """\ This method is called when the connection is opened. |stub-method| """ pass def onDisconnected(self, code, reason=None): """\ This method is called when the connection is closed. |stub-method| :param code: (:class:`int`) The connection closure code to be sent in the WebSocket disconnect frame :param reason: (:class:`str` or :class:`None`) The human readable reason for the closure """ pass def onChange(self, changedPropertyNames): """\ This method is called when a CII message is received from the server that causes one or more of the CII properties to change to a different value. :param changedPropertyNames: A :class:`list` of :class:`str` names of the properties that have changed. Query the :data:`cii` attribute to find out the new values. """ pass def onProtocolError(self, msg): """\ This method is called when there has been an error in the use of the CII protocol - e.g. receiving the wrong kind of message. |stub-method| :param msg: A :class:`str` description of the problem. """ pass def onCiiReceived(self, newCii): """\ This method is called when a CII message is received, but before any 'onXXXXChange()' handlers (if any) are called. It is called even if the message does not result in a change to CII state held locally. By preference is recommended to use the 'onXXXXChange()' handlers instead since these will only be called if there is an actual change to the value of a property in CII state. |stub-method| :param cii: A :class:`~dvbcss.protocol.cii.CII` object representing the received message. """ pass def connect(self): """\ Start the client by trying to open the connection. :throws ConnectionError: There was a problem that meant it was not possible to connect. """ self._conn.connect() def disconnect(self): """\ Disconnect from the server. """ self._conn.disconnect() def _onConnectionOpen(self): self.connected=True self.onConnected() def _onConnectionClose(self, code, reason): self.connected=False self.onDisconnected() def _onProtocolError(self, msg): self.log.error("There was a protocol error: "+msg+". Continuing anyway.") self.onProtocolError(msg) def _onCII(self, newCII): self.latestCII = newCII self.onCiiReceived(newCII) # take a diff since we cannot assume the received message is a diff diff=CII.diff(self.cii, newCII) changes=diff.definedProperties() if len(changes) > 0: self.log.debug("Changed properties: "+ " ".join(changes)) self.cii.update(diff) # now we examine changes and fire change specific callbacks as well as a general callback for name in changes: if name in changes: funcname = self._callBackFuncNames[name] callback = getattr(self, funcname) if callback is not None: newValue=getattr(diff, name) callback(newValue) # fire general catch-all callback self.onChange(changes) else: self.log.debug("No properties have changed") def getStatusSummary(self): if self.latestCII is None: return "Nothing received from TV yet." return str(self.cii)
def test_pack_unpack_timelines(self): TIMELINES=[ ( [], [] ), ( [ TimelineOption("urn:dvb:css:timeline:pts", 1, 1000, 0.2, OMIT) ], [ { "timelineSelector" : "urn:dvb:css:timeline:pts", "timelineProperties" : { "unitsPerTick" : 1, "unitsPerSecond" : 1000, "accuracy" : 0.2 } } ] ), ( [ TimelineOption("urn:dvb:css:timeline:pts", 1, 1000, 0.2, OMIT), TimelineOption("urn:dvb:css:timeline:temi:1:5", 1001, 30000, OMIT, []), TimelineOption("urn:dvb:css:timeline:temi:1:6", 1, 25, OMIT, [{'type':'blah','abc':5},{'type':'bbc','pqr':None}]), ], [ { "timelineSelector" : "urn:dvb:css:timeline:pts", "timelineProperties" : { "unitsPerTick" : 1, "unitsPerSecond" : 1000, "accuracy" : 0.2 } }, { "timelineSelector" : "urn:dvb:css:timeline:temi:1:5", "timelineProperties" : { "unitsPerTick" : 1001, "unitsPerSecond" : 30000 }, "private" : [] }, { "timelineSelector" : "urn:dvb:css:timeline:temi:1:6", "timelineProperties" : { "unitsPerTick" : 1, "unitsPerSecond" : 25, }, "private" : [{'type':'blah','abc':5},{'type':'bbc','pqr':None}] } ] ), ] for (VALUE, ENCODED) in TIMELINES: c=CII(timelines=VALUE) self.assertEquals(c.timelines, VALUE) msg=c.pack() j=json.loads(msg) self.assertEquals(j["timelines"], ENCODED) self.assertEquals(len(j.keys()), 1) d=CII.unpack(msg) self.assertEquals(OMIT, d.protocolVersion) self.assertEquals(OMIT, d.contentId) self.assertEquals(OMIT, d.contentIdStatus) self.assertEquals(OMIT, d.presentationStatus) self.assertEquals(OMIT, d.mrsUrl) self.assertEquals(OMIT, d.wcUrl) self.assertEquals(OMIT, d.tsUrl) self.assertEquals(OMIT, d.teUrl) self.assertEquals(VALUE, d.timelines) self.assertEquals(OMIT, d.private)
if callback is not None: newValue=getattr(diff, name) callback(newValue) # fire general catch-all callback self.onChange(changes) else: self.log.debug("No properties have changed") def getStatusSummary(self): if self.latestCII is None: return "Nothing received from TV yet." return str(self.cii) # programmatically create the onXXXChange methods for every property in a CII message for propertyName in CII.allProperties(): def f(self, newValue): pass f.__doc__="Called when the "+propertyName+" property of the CII message has been changed by a state update from the CII Server.\n\n" + \ "|stub-method|\n\n" + \ ":param newValue: The new value for this property." setattr(CIIClient, "on"+propertyName[0].upper() + propertyName[1:]+"Change", f) __all__ = [ "CIIClientConnection", "CIIClient", ]
def mock_clientConnects(self): """Simulate a client connecting, returns a handle representing that client.""" mockSock = object() self._connections[mockSock] = {"prevCII": CII()} self.onClientConnect(mockSock) return mockSock
class CIIClient(object): """\ Manages a CSS-CII protocol connection to a CSS-CII Server and notifies of changes to CII state. Use by subclassing and overriding the following methods: * :func:`onConnected` * :func:`onDisconnected` * :func:`onChange` * individual `onXXXXChange()` methods named after each CII property * :func:`onCiiReceived` (do not use, by preference) If you do not wish to subclass, you can instead create an instance of this class and replace the methods listed above with your own functions dynamically. The :func:`connect` and :func:`disconnect` methods connect and disconnect the connection to the server and :func:`getStatusSummary` provides a human readable summary of CII state. This object also provides properties you can query: * :data:`cii` represents the current state of CII at the server * :data:`latestCII` is the most recently CII message received from the server * :data:`connected` indicates whether the connection is currently connect """ def __init__(self, ciiUrl): """\ **Initialisation takes the following parameters:** :param ciiUrl: (:class:`str`) The WebSocket URL of the CSS-CII Server (e.g. "ws://127.0.0.1/myservice/cii") """ super(CIIClient, self).__init__() self.log = logging.getLogger("dvbcss.protocol.client.cii.CIIClient") self._conn = CIIClientConnection(ciiUrl) self._conn.onCII = self._onCII self._conn.onConnected = self._onConnectionOpen self._conn.onDisconnected = self._onConnectionClose self._conn.onProtocolError = self._onProtocolError self.connected = False #: True if currently connected to the server, otherwise False. self.cii = CII( ) #: (:class:`~dvbcss.protocol.cii.CII`) CII object representing the CII state at the server self.latestCII = None #: (:class:`~dvbcss.protocol.cii.CII` or :class:`None`) The most recent CII message received from the server or None if nothing has yet been received. self._callBackFuncNames = {} for name in CII.allProperties(): funcname = "on" + name[0].upper() + name[1:] + "Change" self._callBackFuncNames[name] = funcname def onConnected(self): """\ This method is called when the connection is opened. |stub-method| """ pass def onDisconnected(self, code, reason=None): """\ This method is called when the connection is closed. |stub-method| :param code: (:class:`int`) The connection closure code to be sent in the WebSocket disconnect frame :param reason: (:class:`str` or :class:`None`) The human readable reason for the closure """ pass def onChange(self, changedPropertyNames): """\ This method is called when a CII message is received from the server that causes one or more of the CII properties to change to a different value. :param changedPropertyNames: A :class:`list` of :class:`str` names of the properties that have changed. Query the :data:`cii` attribute to find out the new values. """ pass def onProtocolError(self, msg): """\ This method is called when there has been an error in the use of the CII protocol - e.g. receiving the wrong kind of message. |stub-method| :param msg: A :class:`str` description of the problem. """ pass def onCiiReceived(self, newCii): """\ This method is called when a CII message is received, but before any 'onXXXXChange()' handlers (if any) are called. It is called even if the message does not result in a change to CII state held locally. By preference is recommended to use the 'onXXXXChange()' handlers instead since these will only be called if there is an actual change to the value of a property in CII state. |stub-method| :param cii: A :class:`~dvbcss.protocol.cii.CII` object representing the received message. """ pass def connect(self): """\ Start the client by trying to open the connection. :throws ConnectionError: There was a problem that meant it was not possible to connect. """ self._conn.connect() def disconnect(self): """\ Disconnect from the server. """ self._conn.disconnect() def _onConnectionOpen(self): self.connected = True self.onConnected() def _onConnectionClose(self, code, reason): self.connected = False self.onDisconnected() def _onProtocolError(self, msg): self.log.error("There was a protocol error: " + msg + ". Continuing anyway.") self.onProtocolError(msg) def _onCII(self, newCII): self.latestCII = newCII self.onCiiReceived(newCII) # take a diff since we cannot assume the received message is a diff diff = CII.diff(self.cii, newCII) changes = diff.definedProperties() if len(changes) > 0: self.log.debug("Changed properties: " + " ".join(changes)) self.cii.update(diff) # now we examine changes and fire change specific callbacks as well as a general callback for name in changes: if name in changes: funcname = self._callBackFuncNames[name] callback = getattr(self, funcname) if callback is not None: newValue = getattr(diff, name) callback(newValue) # fire general catch-all callback self.onChange(changes) else: self.log.debug("No properties have changed") def getStatusSummary(self): if self.latestCII is None: return "Nothing received from TV yet." return str(self.cii)
if args.quiet: logging.disable(logging.CRITICAL) else: logging.basicConfig(level=args.loglevel[0]) cii = CIIClient(ciiUrl) # logger for outputting messages ciiClientLogger = logging.getLogger("CIIClient") # attach callbacks to generate notifications cii.onConnected = makeCallback("connected") cii.onDisconnected = makeCallback("disconnected") cii.onError = makeCallback("error") for name in CII.allProperties(): funcname = "on" + name[0].upper() + name[1:] + "Change" callback = makePropertyChangeCallback(name) setattr(cii, funcname, callback) # specific handler for when a CII 'change' notification callback fires def onChange(changes): ciiClientLogger.info("CII is now: " + str(cii.cii)) cii.onChange = onChange # connect and goto sleep. All callback activity happens in the websocket's own thread cii.connect() while True: time.sleep(1)
newValue = getattr(diff, name) callback(newValue) # fire general catch-all callback self.onChange(changes) else: self.log.debug("No properties have changed") def getStatusSummary(self): if self.latestCII is None: return "Nothing received from TV yet." return str(self.cii) # programmatically create the onXXXChange methods for every property in a CII message for propertyName in CII.allProperties(): def f(self, newValue): pass f.__doc__="Called when the "+propertyName+" property of the CII message has been changed by a state update from the CII Server.\n\n" + \ "|stub-method|\n\n" + \ ":param newValue: The new value for this property." setattr(CIIClient, "on" + propertyName[0].upper() + propertyName[1:] + "Change", f) __all__ = [ "CIIClientConnection", "CIIClient", ]
"/ts": { 'tools.dvb_ts.on': True, 'tools.dvb_ts.handler_cls': tsServer.handler } }) ciiServer.cii = CII( protocolVersion="1.1", contentId=CONTENT_ID, contentIdStatus="final", presentationStatus=["okay"], mrsUrl=OMIT, tsUrl= "ws://{{host}}:{{port}}/ts", # host & port rewriting has been enabled on the CII server wcUrl="udp://{{host}}:%d" % args. wc_port, # host & port rewriting has been enabled on the CII server teUrl=OMIT, timelines=[ TimelineOption("urn:dvb:css:timeline:pts", unitsPerTick=1, unitsPerSecond=90000), TimelineOption("urn:dvb:css:timeline:temi:1:1", unitsPerTick=1, unitsPerSecond=50) ]) ptsTimeline = CorrelatedClock(parentClock=wallClock, tickRate=90000, correlation=Correlation(wallClock.ticks, 0)) temiTimeline = CorrelatedClock(parentClock=ptsTimeline, tickRate=50,
logging.disable(logging.CRITICAL) else: logging.basicConfig(level=args.loglevel[0]) cii = CIIClient(ciiUrl) # logger for outputting messages ciiClientLogger = logging.getLogger("CIIClient") # attach callbacks to generate notifications cii.onConnected = makeCallback("connected") cii.onDisconnected = makeCallback("disconnected") cii.onError = makeCallback("error"); for name in CII.allProperties(): funcname="on" + name[0].upper() + name[1:] + "Change" callback = makePropertyChangeCallback(name) setattr(cii, funcname, callback) # specific handler for when a CII 'change' notification callback fires def onChange(changes): ciiClientLogger.info("CII is now: "+str(cii.cii)) cii.onChange = onChange # connect and goto sleep. All callback activity happens in the websocket's own thread cii.connect() while True: time.sleep(1)