--- /dev/null
+from netgrowl import GrowlRegistrationPacket, GrowlNotificationPacket, \
+ GROWL_UDP_PORT, md5_constructor
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import reactor
+from struct import unpack
+
+from Tools import Notifications
+from Components.config import config
+
+from GrowleeConnection import emergencyDisable
+from . import NOTIFICATIONID
+
+class GrowlTalk(DatagramProtocol):
+ addr = None
+
+ def gotIP(self, ip):
+ self.addr = (ip, GROWL_UDP_PORT)
+ if config.plugins.growlee.enable_outgoing.value:
+ p = GrowlRegistrationPacket(application="growlee", password=config.plugins.growlee.password.value)
+ p.addNotification()
+ payload = p.payload()
+
+ self.transport.write(payload, self.addr)
+
+ def noIP(self, error):
+ emergencyDisable()
+
+ def startProtocol(self):
+ reactor.resolve(config.plugins.growlee.address.value).addCallback(self.gotIP).addErrback(self.noIP)
+
+ def sendNotification(self, *args, **kwargs):
+ if not self.transport or not self.addr or not config.plugins.growlee.enable_outgoing.value:
+ return
+
+ kwargs["application"] = "growlee"
+ kwargs["password"] = config.plugins.growlee.password.value
+ p = GrowlNotificationPacket(*args, **kwargs)
+ payload = p.payload()
+
+ self.transport.write(payload, self.addr)
+
+ def datagramReceived(self, data, addr):
+ if not config.plugins.growlee.enable_incoming.value:
+ return
+
+ Len = len(data)
+ if Len < 16:
+ return
+
+ # ver == GROWL_PROTOCOL_VERSION
+ if data[0] != '\x01':
+ return
+
+ # type == GROWL_TYPE_NOTIFICATION
+ if data[1] == '\x01':
+ digest = data[-16:]
+ password = config.plugins.growlee.password.value
+ checksum = md5_constructor()
+ checksum.update(data[:-16])
+ if password:
+ checksum.update(password)
+ if digest != checksum.digest():
+ return
+
+ nlen, tlen, dlen, alen = unpack("!HHHH",str(data[4:12]))
+ notification, title, description = unpack("%ds%ds%ds" % (nlen, tlen, dlen), data[12:Len-alen-16])
+ # type == GROWL_TYPE_NOTIFICATION_NOAUTH
+ elif data[1] == '\x05':
+ nlen, tlen, dlen, alen = unpack("!HHHH",str(data[4:12]))
+ notification, title, description = unpack("%ds%ds%ds" % (nlen, tlen, dlen), data[12:Len-alen])
+ else:
+ # don't handle any other packet yet
+ return
+
+ Notifications.AddNotificationWithID(
+ NOTIFICATIONID,
+ MessageBox,
+ text = title + '\n' + description,
+ type = MessageBox.TYPE_INFO,
+ timeout = 5,
+ close_on_any_key = True,
+ )
+
+class GrowlTalkAbstraction:
+ def __init__(self):
+ self.growltalk = GrowlTalk()
+ self.serverPort = reactor.listenUDP(GROWL_UDP_PORT, self.growltalk)
+
+ def sendNotification(self, *args, **kwargs):
+ # map timeout -> sticky
+ if "timeout" in kwargs:
+ if kwargs["timeout"] == -1:
+ kwargs["sticky"] = True
+ del kwargs["timeout"]
+
+ self.growltalk.sendNotification(*args, **kwargs)
+
+ def stop(self):
+ return self.serverPort.stopListening()
+
--- /dev/null
+from Components.config import config
+from Tools import Notifications
+from Screens.MessageBox import MessageBox
+
+from . import NOTIFICATIONID
+
+def emergencyDisable(*args, **kwargs):
+ global growleeConnection
+ if growleeConnection:
+ growleeConnection.stop()
+
+ if gotNotification in Notifications.notificationAdded:
+ Notifications.notificationAdded.remove(gotNotification)
+ Notifications.AddPopup(
+ _("Network error.\nDisabling Growlee until next restart!"),
+ MessageBox.TYPE_ERROR,
+ 10
+ )
+
+def gotNotification():
+ notifications = Notifications.notifications
+ if notifications:
+ _, screen, args, kwargs, id = notifications[-1]
+ if screen is MessageBox and id != NOTIFICATIONID and id not in config.plugins.growlee.blacklist.value:
+
+ # NOTE: priority is in [-2; 2] but type is [0; 3] so map it
+ # XXX: maybe priority==type-2 would be more appropriate
+ priority = kwargs.get("type", 0) - 1
+ timeout = kwargs.get("timeout", -1)
+
+ if "text" in kwargs:
+ description = kwargs["text"]
+ else:
+ description = args[0]
+ description = description.decode('utf-8')
+
+ growleeConnection.sendNotification(title="Dreambox", description=description, priority=priority, timeout=timeout)
+
+class GrowleeConnection:
+ connection = None
+
+ def sendNotification(self, title="Dreambox", description='', priority=-1, timeout = -1):
+ if self.connection and not priority < config.plugins.growlee.level.value:
+ self.connection.sendNotification(title=title, description=description, priority=priority, timeout=timeout)
+
+ def listen(self):
+ if self.connection:
+ return
+
+ proto = config.plugins.growlee.protocol.value
+ if proto == "prowl":
+ from Prowl import ProwlAPI
+ self.connection = ProwlAPI()
+ elif proto == "growl":
+ from GrowlTalk import GrowlTalkAbstraction
+ self.connection = GrowlTalkAbstraction()
+ else: # proto == "snarl":
+ from SNP import SnarlNetworkProtocolAbstraction
+ self.connection = SnarlNetworkProtocolAbstraction()
+
+ def stop(self):
+ if self.connection:
+ d = self.connection.stop()
+ self.connection = None
+ return d
+ return None
+
+growleeConnection = GrowleeConnection()
+
--- /dev/null
+from twisted.web.client import getPage
+from twisted.internet.defer import Deferred
+from twisted.internet import reactor
+from urllib import urlencode
+
+from Components.config import config
+
+from GrowleeConnection import emergencyDisable
+from . import NOTIFICATIONID
+
+class ProwlAPI:
+ def sendNotification(self, title='No title.', description='No message.', priority=0, timeout=-1):
+ if not config.plugins.growlee.enable_outgoing.value:
+ return
+
+ headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'}
+ data = {
+ 'apikey': config.plugins.growlee.prowl_api_key.value,
+ 'application': "growlee",
+ 'event': title,
+ 'description': description,
+ 'priority': priority,
+ }
+
+ getPage('https://prowl.weks.net/publicapi/add/', method = 'POST', headers = headers, postdata = urlencode(data)).addErrback(emergencyDisable)
+
+ def stop(self):
+ defer = Deferred()
+ reactor.callLater(1, defer.callback, True)
+ return defer
+
--- /dev/null
+from netgrowl import GROWL_UDP_PORT
+from twisted.internet.defer import Deferred
+from twisted.internet.protocol import ClientFactory, ServerFactory
+from twisted.internet import reactor
+from twisted.protocols.basic import LineReceiver
+
+from Tools import Notifications
+from Components.config import config
+
+from GrowleeConnection import emergencyDisable
+from . import NOTIFICATIONID
+
+class SnarlNetworkProtocol(LineReceiver):
+ def __init__(self, client = False):
+ self.client = client
+
+ def connectionMade(self):
+ self.factory.addClient(self)
+ if self.client:
+ payload = "type=SNP#?version=1.0#?action=register#?app=growlee"
+ self.sendLine(payload)
+
+ payload = "type=SNP#?version=1.0#?action=add_class#?app=growlee#?class=growleeClass#?title=Notifications from your Dreambox"
+ self.sendLine(payload)
+
+ def connectionLost(self, reason):
+ self.factory.removeClient(self)
+
+ def stop(self):
+ if self.client:
+ payload = "type=SNP#?version=1.0#?action=unregister#?app=growlee"
+ self.sendLine(payload)
+
+ self.transport.loseConnection()
+
+ def sendNotification(self, title='No title.', description='No message.', timeout=1):
+ if not self.client or not self.transport:
+ return
+
+ payload = "type=SNP#?version=1.0#?action=notification#?app=growlee#?class=growleeClass#?title=%s#?text=%s#?timeout=%d" % (title, description, timeout)
+ self.sendLine(payload)
+
+ def lineReceived(self, data):
+ if self.client or not self.transport:
+ return
+
+ Len = len(data)
+ if Len < 23 or not data[:23] == "type=SNP#?version=1.0#?":
+ return
+
+ items = data[23:].split('#?')
+
+ title = ''
+ description = ''
+ timeout = 5
+ for item in items:
+ key, value = item.split('=')
+ if key == "action":
+ if value == "unregister":
+ payload = "SNP/1.0/0/OK"
+ self.sendLine(payload)
+ self.transport.loseConnection()
+ return
+ elif value != "notification":
+ # NOTE: we pretend to handle&accept pretty much everything one throws at us
+ payload = "SNP/1.0/0/OK"
+ self.sendLine(payload)
+ return
+ elif key == "title":
+ title = value
+ elif key == "text":
+ description = value
+ elif key == "timeout":
+ timeout = int(value)
+
+ Notifications.AddNotificationWithID(
+ NOTIFICATIONID,
+ MessageBox,
+ text = title + '\n' + description,
+ type = MessageBox.TYPE_INFO,
+ timeout = timeout,
+ close_on_any_key = True,
+ )
+
+ # return ok
+ payload = "SNP/1.0/0/OK"
+ self.sendLine(payload)
+
+class SnarlNetworkProtocolClientFactory(ClientFactory):
+ client = None
+
+ def buildProtocol(self, addr):
+ p = SnarlNetworkProtocol(client = True)
+ p.factory = self
+ return p
+
+ def sendNotification(self, title='No title.', description='No message.', priority=0, timeout=-1):
+ if self.client:
+ title = title.decode('utf-8', 'ignore').encode('iso8859-15', 'ignore')
+ description = description.decode('utf-8', 'ignore').encode('iso8859-15', 'ignore')
+
+ # NOTE: timeout = 0 means sticky, so add one second to map -1 to 0 and make 0 non-sticky
+ if timeout < 1:
+ timeout += 1
+
+ self.client.sendNotification(title=title, description=description, timeout=timeout)
+
+ def addClient(self, client):
+ self.client = client
+
+ def removeClient(self, client):
+ self.client = None
+
+class SnarlNetworkProtocolServerFactory(ServerFactory):
+ protocol = SnarlNetworkProtocol
+
+ def __init__(self):
+ self.clients = []
+
+ def addClient(self, client):
+ self.clients.append(client)
+
+ def removeClient(self, client):
+ self.clients.remove(client)
+
+ def sendNotification(self, *args, **kwargs):
+ pass
+
+ def stopFactory(self):
+ for client in self.clients:
+ client.stop()
+
+class SnarlNetworkProtocolAbstraction:
+ clientPort = None
+ serverPort = None
+ pending = 0
+
+ def __init__(self):
+ self.clientFactory = SnarlNetworkProtocolClientFactory()
+ self.serverFactory = SnarlNetworkProtocolServerFactory()
+
+ if config.plugins.growlee.enable_outgoing.value:
+ reactor.resolve(config.plugins.growlee.address.value).addCallback(self.gotIP).addErrback(self.noIP)
+
+ if config.plugins.growlee.enable_incoming.value:
+ self.serverPort = reactor.listenTCP(GROWL_UDP_PORT, self.serverFactory)
+ self.pending += 1
+
+ def gotIP(self, ip):
+ self.clientPort = reactor.connectTCP(ip, GROWL_UDP_PORT, self.clientFactory)
+ self.pending += 1
+
+ def noIP(self, error):
+ emergencyDisable()
+
+ def sendNotification(self, *args, **kwargs):
+ self.clientFactory.sendNotification(*args, **kwargs)
+
+ def maybeClose(self, resOrFail, defer = None):
+ self.pending -= 1
+ if self.pending == 0:
+ if defer:
+ defer.callback(True)
+
+ def stop(self):
+ defer = Deferred()
+ if self.clientPort:
+ d = self.clientPort.disconnect()
+ if d:
+ d.addBoth(self.maybeClose, defer = defer)
+ else:
+ self.pending -= 1
+
+ if self.serverPort:
+ d = self.serverPort.stopListening()
+ if d:
+ d.addBoth(self.maybeClose, defer = defer)
+ else:
+ self.pending -= 1
+
+ if self.pending == 0:
+ reactor.callLater(1, defer.callback, True)
+ return defer
+
+NOTIFICATIONID = 'GrowleeReceivedNotification'
+
from Plugins.Plugin import PluginDescriptor
from Tools import Notifications
-from netgrowl import GrowlRegistrationPacket, GrowlNotificationPacket, \
- GROWL_UDP_PORT, md5_constructor
-from twisted.internet.protocol import DatagramProtocol
-from twisted.internet import reactor
-from twisted.web.client import getPage
-from struct import unpack
-from socket import gaierror
-from urllib import urlencode
from Screens.Setup import SetupSummary
from Screens.Screen import Screen
-from Screens.MessageBox import MessageBox
from Components.ActionMap import ActionMap
from Components.config import config, getConfigListEntry, ConfigSubsection, \
ConfigText, ConfigPassword, ConfigYesNo, ConfigSelection, ConfigSet
from Components.ConfigList import ConfigListScreen
from Components.Sources.StaticText import StaticText
+from GrowleeConnection import gotNotification, emergencyDisable, growleeConnection
+
+from . import NOTIFICATIONID
+
config.plugins.growlee = ConfigSubsection()
config.plugins.growlee.enable_incoming = ConfigYesNo(default=False)
config.plugins.growlee.enable_outgoing = ConfigYesNo(default=False)
config.plugins.growlee.password = ConfigPassword()
config.plugins.growlee.prowl_api_key = ConfigText(fixed_size=False)
config.plugins.growlee.protocol = ConfigSelection(default="growl", choices = [("growl", "Growl"), ("snarl", "Snarl"), ("prowl", "Prowl")])
+config.plugins.growlee.level = ConfigSelection(default=-1, choices = [(-1, _("Low (Yes/No)")), (0, _("Normal (Information)")), (1, _("High (Warning)")), (2, _("Highest (Emergency)"))])
config.plugins.growlee.blacklist = ConfigSet(choices = [])
-NOTIFICATIONID = 'GrowleeReceivedNotification'
-
class GrowleeConfiguration(Screen, ConfigListScreen):
def __init__(self, session):
Screen.__init__(self, session)
def setupList(self, *args):
l = [
getConfigListEntry(_("Type"), config.plugins.growlee.protocol),
+ getConfigListEntry(_("Minimum Priority"), config.plugins.growlee.level),
getConfigListEntry(_("Send Notifications?"), config.plugins.growlee.enable_outgoing),
]
- if config.plugins.growlee.protocol.value == "prowl":
+ proto = config.plugins.growlee.protocol.value
+ if proto == "prowl":
l.append(getConfigListEntry(_("API Key"), config.plugins.growlee.prowl_api_key))
else:
l.extend((
getConfigListEntry(_("Receive Notifications?"), config.plugins.growlee.enable_incoming),
getConfigListEntry(_("Address"), config.plugins.growlee.address),
- getConfigListEntry(_("Password"), config.plugins.growlee.password),
))
+ if proto == "growl":
+ l.append(
+ getConfigListEntry(_("Password"), config.plugins.growlee.password)
+ )
self["config"].list = l
def keySave(self):
if self["config"].isChanged():
- global port
- if port:
- def maybeConnect(*args, **kwargs):
- if config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
- doConnect()
+ def maybeConnect(*args, **kwargs):
+ if config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
+ growleeConnection.listen()
- d = port.stopListening()
- if d is not None:
- d.addCallback(maybeConnect).addErrback(emergencyDisable)
- else:
- maybeConnect()
- elif config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
- doConnect()
+ d = growleeConnection.stop()
+ d.addCallback(maybeConnect).addErrback(emergencyDisable)
self.saveAll()
self.close()
def configuration(session, **kwargs):
session.open(GrowleeConfiguration)
-def doConnect():
- global port
- if config.plugins.growlee.protocol.value == "snarl":
- port = reactor.listenTCP(GROWL_UDP_PORT, growlProtocolOneWrapper)
- else:
- port = reactor.listenUDP(GROWL_UDP_PORT, growlProtocolOneWrapper)
-
-def emergencyDisable(*args, **kwargs):
- global port
- if port:
- port.stopListening()
- port = None
-
- if gotNotification in Notifications.notificationAdded:
- Notifications.notificationAdded.remove(gotNotification)
- Notifications.AddPopup(
- _("Network error.\nDisabling Growlee!"),
- MessageBox.TYPE_ERROR,
- 10
- )
-
-class GrowlProtocolOneWrapper(DatagramProtocol):
- def startProtocol(self):
- proto = config.plugins.growlee.protocol.value
- if config.plugins.growlee.enable_outgoing.value and not proto == "prowl":
- addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
- if proto == "growl":
- p = GrowlRegistrationPacket(application="growlee", password=config.plugins.growlee.password.value)
- p.addNotification()
- payload = p.payload()
- else: #proto == "snarl":
- payload = "type=SNP#?version=1.0#?action=register#?app=growlee\r\n"
- try:
- self.transport.write(payload, addr)
- except gaierror:
- emergencyDisable()
-
- def doStop(self):
- if config.plugins.growlee.enable_outgoing.value and config.plugins.growlee.protocol.value == "snarl":
- addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
- payload = "type=SNP#?version=1.0#?action=unregister#?app=growlee\r\n"
- try:
- self.transport.write(payload, addr)
- except gaierror:
- pass
- DatagramProtocol.doStop(self)
-
- def sendNotification(self, *args, **kwargs):
- if not self.transport or not config.plugins.growlee.enable_outgoing.value:
- return
-
- proto = config.plugins.growlee.protocol.value
- if proto == "prowl":
- headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'}
- data = {
- 'apikey': config.plugins.growlee.prowl_api_key.value,
- 'application': "growlee",
- 'event': kwargs.get('title', 'No title.'),
- 'description': kwargs.get('description', 'No message.'),
- 'priority': kwargs.get('priority', 0),
- }
-
- getPage('https://prowl.weks.net/publicapi/add/', method = 'POST', headers = headers, postdata = urlencode(data)).addErrback(emergencyDisable)
- else:
- addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
- if proto == "growl":
- # map timeout -> sticky
- if "timeout" in kwargs:
- if kwargs["timeout"] == -1:
- kwargs["sticky"] = True
- del kwargs["timeout"]
-
- p = GrowlNotificationPacket(*args, application="growlee", **kwargs)
- payload = p.payload()
- else: #proto == "snarl":
- title = kwargs.get('title', 'No title.')
- text = kwargs.get('description', 'No message.')
- timeout = kwargs.get('timeout', -1)
-
- # NOTE: timeout = 0 means sticky, so add one second to map -1 to 0 and make 0 non-sticky
- if timeout < 2:
- timeout += 1
-
- payload = "type=SNP#?version=1.0#?action=notification#?app=growlee#?class=growleeClass#?title=%s#?text=%s#?timeout=%d\r\n" % (title, text, timeout)
- try:
- self.transport.write(payload, addr)
- except gaierror:
- emergencyDisable()
-
- def datagramReceived(self, data, addr):
- proto = config.plugins.growlee.protocol.value
- if proto == "prowl" or not config.plugins.growlee.enable_incoming.value:
- return
-
- Len = len(data)
- if proto == "growl":
- if Len < 16:
- return
-
- digest = data[-16:]
- password = config.plugins.growlee.password.value
- checksum = md5_constructor()
- checksum.update(data[:-16])
- if password:
- checksum.update(password)
- if digest != checksum.digest():
- return
-
- # notify packet
- if data[1] == '\x01':
- nlen, tlen, dlen, alen = unpack("!HHHH",str(data[4:12]))
- notification, title, description = unpack("%ds%ds%ds" % (nlen, tlen, dlen), data[12:Len-alen-16])
-
- Notifications.AddNotificationWithID(
- NOTIFICATIONID,
- MessageBox,
- text = title + '\n' + description,
- type = MessageBox.TYPE_INFO,
- timeout = 5,
- close_on_any_key = True,
- )
-
- # TODO: do we want to handle register packets? :-)
- else: #proto == "snarl":
- if Len < 23 or not data[:23] == "type=SNP#?version=1.0#?":
- return
-
- items = data[23:].split('#?')
-
- title = ''
- description = ''
- timeout = 5
- for item in items:
- key, value = item.split('=')
- if key == "action":
- if value != "notification":
- # NOTE: we pretend to handle&accept pretty much everything one throws at us
- addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
- payload = "SNP/1.0/0/OK\r\n"
- try:
- self.transport.write(payload, addr)
- except gaierror:
- emergencyDisable()
- return
- elif key == "title":
- title = value
- elif key == "text":
- description = value
- elif key == "timeout":
- timeout = int(value)
-
- Notifications.AddNotificationWithID(
- NOTIFICATIONID,
- MessageBox,
- text = title + '\n' + description,
- type = MessageBox.TYPE_INFO,
- timeout = timeout,
- close_on_any_key = True,
- )
-
- # return ok
- addr = (config.plugins.growlee.address.value, GROWL_UDP_PORT)
- payload = "SNP/1.0/0/OK\r\n"
- try:
- self.transport.write(payload, addr)
- except gaierror:
- emergencyDisable()
-
-
-growlProtocolOneWrapper = GrowlProtocolOneWrapper()
-port = None
-
-def gotNotification():
- notifications = Notifications.notifications
- if notifications:
- _, screen, args, kwargs, id = notifications[-1]
- if screen is MessageBox and id != NOTIFICATIONID and id not in config.plugins.growlee.blacklist.value:
-
- type = kwargs.get("type", 0)
- timeout = kwargs.get("timeout", -1)
-
- if "text" in kwargs:
- description = kwargs["text"]
- else:
- description = args[0]
- description = description.decode('utf-8')
-
- # NOTE: priority is in [-2; 2] but type is [0; 3] so map it
- # XXX: maybe priority==type-2 would be more appropriate
- growlProtocolOneWrapper.sendNotification(title="Dreambox", description=description, password=config.plugins.growlee.password.value, priority=type-1, timeout=timeout)
-
def autostart(**kwargs):
- if config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
- doConnect()
-
# NOTE: we need to be the first one to be notified since other listeners
# may remove the notifications from the list for good
Notifications.notificationAdded.insert(0, gotNotification)
+ if config.plugins.growlee.enable_incoming.value or config.plugins.growlee.enable_outgoing.value:
+ growleeConnection.listen()
+
def Plugins(**kwargs):
return [
PluginDescriptor(