Major refactoring #2

This commit is contained in:
Fabio Manganiello 2017-12-18 01:10:51 +01:00
parent 546ea1b9b9
commit 4a04e51da7
12 changed files with 308 additions and 188 deletions

View File

@ -1,16 +1,13 @@
import functools
import importlib
import os
import logging import logging
import socket
import sys import sys
import traceback import traceback
import yaml
from queue import Queue
from threading import Thread from threading import Thread
from getopt import getopt from getopt import getopt
from .bus import Bus
from .config import Config
from .utils import get_or_load_plugin, init_backends
from .message.request import Request from .message.request import Request
from .message.response import Response from .message.response import Response
@ -19,50 +16,13 @@ __version__ = '0.3.3'
#-----------# #-----------#
config = {}
modules = {}
wrkdir = os.path.dirname(os.path.realpath(__file__))
def _init_plugin(plugin_name, reload=False):
global modules
global config
if plugin_name in modules and not reload:
return modules[plugin_name]
try:
module = importlib.import_module(__package__ + '.plugins.' + plugin_name)
except ModuleNotFoundError as e:
logging.warn('No such plugin: {}'.format(plugin_name))
raise RuntimeError(e)
# e.g. plugins.music.mpd main class: MusicMpdPlugin
cls_name = functools.reduce(
lambda a,b: a.title() + b.title(),
(plugin_name.title().split('.'))
) + 'Plugin'
plugin_conf = config[plugin_name] if plugin_name in config else {}
try:
plugin = getattr(module, cls_name)(plugin_conf)
modules[plugin_name] = plugin
except AttributeError as e:
logging.warn('No such class in {}: {}'.format(
plugin_name, cls_name))
raise RuntimeError(e)
return plugin
def _execute_request(request, retry=True): def _execute_request(request, retry=True):
tokens = request.action.split('.') tokens = request.action.split('.')
module_name = str.join('.', tokens[:-1]) module_name = str.join('.', tokens[:-1])
method_name = tokens[-1:][0] method_name = tokens[-1:][0]
try: try:
plugin = _init_plugin(module_name) plugin = get_or_load_plugin(module_name)
except RuntimeError as e: # Module/class not found except RuntimeError as e: # Module/class not found
logging.exception(e) logging.exception(e)
return return
@ -78,7 +38,7 @@ def _execute_request(request, retry=True):
logging.exception(e) logging.exception(e)
if retry: if retry:
logging.info('Reloading plugin {} and retrying'.format(module_name)) logging.info('Reloading plugin {} and retrying'.format(module_name))
_init_plugin(module_name, reload=True) get_or_load_plugin(module_name, reload=True)
_execute_request(request, retry=False) _execute_request(request, retry=False)
finally: finally:
# Send the response on the backend that received the request # Send the response on the backend that received the request
@ -95,131 +55,32 @@ def on_msg(msg):
logging.info('Received response: {}'.format(msg)) logging.info('Received response: {}'.format(msg))
def parse_config_file(config_file=None):
global config
if config_file:
locations = [config_file]
else:
locations = [
# ./config.yaml
os.path.join(wrkdir, 'config.yaml'),
# ~/.config/platypush/config.yaml
os.path.join(os.environ['HOME'], '.config', 'platypush', 'config.yaml'),
# /etc/platypush/config.yaml
os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'),
]
for loc in locations:
try:
with open(loc,'r') as f:
config = yaml.load(f)
except FileNotFoundError as e:
pass
for section in config:
if 'disabled' in config[section] and config[section]['disabled']:
del config[section]
if 'logging' not in config:
config['logging'] = logging.INFO
else:
config['logging'] = getattr(logging, config['logging'].upper())
if 'device_id' not in config:
config['device_id'] = socket.gethostname()
return config
def init_backends(config, bus=None):
backends = {}
for k in config.keys():
if not k.startswith('backend.'): continue
module = importlib.import_module(__package__ + '.' + k)
# e.g. backend.pushbullet main class: PushbulletBackend
cls_name = functools.reduce(
lambda a,b: a.title() + b.title(),
(module.__name__.title().split('.')[2:])
) + 'Backend'
# Ignore the pusher attribute here
if 'pusher' in config[k]: del config[k]['pusher']
try:
b = getattr(module, cls_name)(bus=bus, **config[k])
name = '.'.join((k.split('.'))[1:])
backends[name] = b
except AttributeError as e:
logging.warn('No such class in {}: {}'.format(
module.__name__, cls_name))
raise RuntimeError(e)
return backends
def get_default_pusher_backend(config):
backends = ['.'.join((k.split('.'))[1:])
for k in config.keys() if k.startswith('backend.')
and 'pusher' in config[k] and config[k]['pusher'] is True]
return backends[0] if backends else None
def get_logging_level():
global config
return config['logging']
def get_device_id():
global config
return config['device_id'] if 'device_id' in config else None
def main(): def main():
print('Starting platypush v.{}'.format(__version__)) print('Starting platypush v.{}'.format(__version__))
debug = False
config_file = None config_file = None
plugins_dir = os.path.join(wrkdir, 'plugins')
sys.path.insert(0, plugins_dir)
optlist, args = getopt(sys.argv[1:], 'vh') optlist, args = getopt(sys.argv[1:], 'vh')
for opt, arg in optlist: for opt, arg in optlist:
if opt == '-c': if opt == '-c':
config_file = arg config_file = arg
if opt == '-v':
debug = True
elif opt == '-h': elif opt == '-h':
print(''' print('''
Usage: {} [-v] [-h] [-c <config_file>] Usage: {} [-h] [-c <config_file>]
-v Enable debug mode
-h Show this help -h Show this help
-c Path to the configuration file (default: ./config.yaml) -c Path to the configuration file (default: ./config.yaml)
'''.format(sys.argv[0])) '''.format(sys.argv[0]))
return return
config = parse_config_file(config_file) Config.init(config_file)
if debug: config['logging'] = logging.DEBUG logging.basicConfig(level=Config.get('logging'), stream=sys.stdout)
logging.basicConfig(level=get_logging_level(), stream=sys.stdout) bus = Bus(on_msg=on_msg)
logging.debug('Configuration dump: {}'.format(config)) backends = init_backends(bus)
bus = Queue()
backends = init_backends(config, bus)
for backend in backends.values(): for backend in backends.values():
backend.start() backend.start()
while True: bus.loop_forever()
try:
on_msg(bus.get())
except KeyboardInterrupt:
return
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -1,11 +1,11 @@
import importlib import importlib
import logging import logging
import sys import sys
import platypush
from queue import Queue from queue import Queue
from threading import Thread from threading import Thread
from platypush.config import Config
from platypush.message import Message from platypush.message import Message
from platypush.message.request import Request from platypush.message.request import Request
from platypush.message.response import Response from platypush.message.response import Response
@ -24,11 +24,11 @@ class Backend(Thread):
# If no bus is specified, create an internal queue where # If no bus is specified, create an internal queue where
# the received messages will be pushed # the received messages will be pushed
self.bus = bus if bus else Queue() self.bus = bus if bus else Queue()
self.device_id = platypush.get_device_id() self.device_id = Config.get('device_id')
self.msgtypes = {} self.msgtypes = {}
Thread.__init__(self) Thread.__init__(self)
logging.basicConfig(stream=sys.stdout, level=platypush.get_logging_level() logging.basicConfig(stream=sys.stdout, level=Config.get('logging')
if 'logging' not in kwargs if 'logging' not in kwargs
else getattr(logging, kwargs['logging'])) else getattr(logging, kwargs['logging']))
@ -87,7 +87,7 @@ class Backend(Thread):
logging.debug('Message received on the backend: {}'.format(msg)) logging.debug('Message received on the backend: {}'.format(msg))
msg.backend = self # Augment message msg.backend = self # Augment message
self.bus.put(msg) self.bus.post(msg)
def send_request(self, request): def send_request(self, request):
""" """

View File

@ -4,6 +4,7 @@ import requests
import time import time
import websocket import websocket
from platypush.config import Config
from platypush.message import Message from platypush.message import Message
from .. import Backend from .. import Backend

23
platypush/bus/__init__.py Normal file
View File

@ -0,0 +1,23 @@
from queue import Queue
class Bus(object):
""" Main local bus where the daemon will listen for new messages """
def __init__(self, on_msg):
self.bus = Queue()
self.on_msg = on_msg
def post(self, msg):
""" Sends a message to the bus """
self.bus.put(msg)
def loop_forever(self):
""" Reads messages from the bus """
while True:
try:
self.on_msg(self.bus.get())
except KeyboardInterrupt:
return
# vim:sw=4:ts=4:et:

View File

@ -0,0 +1,145 @@
import logging
import os
import socket
import yaml
""" Config singleton instance """
_default_config_instance = None
class Config(object):
"""
Configuration base class
Usage:
- Initialize config from one of the default paths:
Config.init()
- Initialize config from a custom path
Config.init(config_file_path)
- Set a value
Config.set('foo', 'bar')
- Get a value
Config.get('foo')
"""
"""
Default config file locations:
- $HOME/.config/platypush/config.yaml
- /etc/platypush/config.yaml
"""
_cfgfile_locations = [
os.path.join(os.environ['HOME'], '.config', 'platypush', 'config.yaml'),
os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'),
]
def __init__(self, cfgfile=None):
"""
Constructor. Always use the class as a singleton (i.e. through
Config.init), you won't probably need to call the constructor directly
Params:
cfgfile -- Config file path (default: retrieve the first
available location in _cfgfile_locations)
"""
if cfgfile is None:
cfgfile = self._get_default_cfgfile()
if cfgfile is None:
raise RuntimeError('No config file specified and nothing found in {}'
.format(self._cfgfile_locations))
with open(cfgfile, 'r') as fp:
self._config = yaml.load(fp)
for section in self._config:
if 'disabled' in self._config[section] \
and self._config[section]['disabled']:
del self._config[section]
if 'logging' not in self._config:
self._config['logging'] = logging.INFO
else:
self._config['logging'] = getattr(logging, self._config['logging'].upper())
if 'device_id' not in self._config:
self._config['device_id'] = socket.gethostname()
self._init_backends()
self._init_plugins()
def _init_backends(self):
self.backends = {}
for key in self._config.keys():
if not key.startswith('backend.'): continue
backend_name = '.'.join(key.split('.')[1:])
self.backends[backend_name] = self._config[key]
def _init_plugins(self):
self.plugins = {}
for key in self._config.keys():
if key.startswith('backend.'): continue
plugin_name = '.'.join(key.split('.')[1:])
self.plugins[plugin_name] = self._config[key]
@staticmethod
def get_backends():
global _default_config_instance
if _default_config_instance is None: _default_config_instance = Config()
return _default_config_instance.backends
@staticmethod
def get_plugins():
global _default_config_instance
if _default_config_instance is None: _default_config_instance = Config()
return _default_config_instance.plugins
@staticmethod
def get_default_pusher_backend():
"""
Gets the default pusher backend from the config
"""
backends = [k for k in Config.get_backends().keys()
if 'pusher' in Config.get_backends()[k]
and Config.get_backends()[k]['pusher'] is True]
return backends[0] if backends else None
@classmethod
def _get_default_cfgfile(cls):
for location in cls._cfgfile_locations:
if os.path.isfile(location): return location
@staticmethod
def init(cfgfile=None):
"""
Initializes the config object singleton
Params:
cfgfile -- path to the config file - default: _cfgfile_locations
"""
global _default_config_instance
_default_config_instance = Config(cfgfile)
@staticmethod
def get(key):
"""
Gets a config value
Params:
key -- Config key to get
"""
global _default_config_instance
if _default_config_instance is None: _default_config_instance = Config()
return _default_config_instance._config[key]
@staticmethod
def set(key, value):
"""
Sets a config value
Params:
key -- Config key to set
value -- Value for key
"""
global _default_config_instance
if _default_config_instance is None: _default_config_instance = Config()
_default_config_instance._config[key] = key
# vim:sw=4:ts=4:et:

View File

@ -25,7 +25,7 @@ class Request(Message):
args = { args = {
'target' : msg['target'], 'target' : msg['target'],
'action' : msg['action'], 'action' : msg['action'],
'args' : msg['args'], 'args' : msg['args'] if 'args' in msg else {},
} }
if 'origin' in msg: args['origin'] = msg['origin'] if 'origin' in msg: args['origin'] = msg['origin']

View File

@ -1,23 +1,16 @@
import os
import sys import sys
import logging import logging
from platypush import get_logging_level from platypush.config import Config
from platypush.message.response import Response from platypush.message.response import Response
class Plugin(object): class Plugin(object):
def __init__(self, config): """ Base plugin class """
self.config = config
logging.basicConfig(stream=sys.stdout, level=get_logging_level()
if 'logging' not in config
else getattr(logging, config.pop('logging')))
for cls in reversed(self.__class__.mro()): def __init__(self, **kwargs):
if cls is not object: logging.basicConfig(stream=sys.stdout, level=Config.get('logging')
try: if 'logging' not in kwargs
cls._init(self) else getattr(logging, kwargs['logging']))
except AttributeError as e:
pass
def run(self, method, *args, **kwargs): def run(self, method, *args, **kwargs):
return getattr(self, method)(*args, **kwargs) return getattr(self, method)(*args, **kwargs)

View File

@ -7,12 +7,24 @@ from platypush.message.response import Response
from .. import LightPlugin from .. import LightPlugin
class LightHuePlugin(LightPlugin): class LightHuePlugin(LightPlugin):
""" Philips Hue lights plugin """
MAX_BRI = 255 MAX_BRI = 255
MAX_SAT = 255 MAX_SAT = 255
MAX_HUE = 65535 MAX_HUE = 65535
def _init(self): def __init__(self, bridge, lights=None, groups=None):
self.bridge_address = self.config['bridge'] """
Constructor
Params:
bridge -- Bridge address or hostname
lights -- Lights to be controlled (default: all)
groups -- Groups to be controlled (default: all)
"""
super().__init__()
self.bridge_address = bridge
self.bridge = None self.bridge = None
logging.info('Initializing Hue lights plugin - bridge: "{}"'. logging.info('Initializing Hue lights plugin - bridge: "{}"'.
format(self.bridge_address)) format(self.bridge_address))
@ -20,20 +32,18 @@ class LightHuePlugin(LightPlugin):
self.connect() self.connect()
self.lights = []; self.groups = [] self.lights = []; self.groups = []
if 'lights' in self.config: if lights:
self.lights = self.config['lights'] self.lights = lights
elif 'groups' in self.config: elif groups:
self.groups = self.config['groups'] self.groups = groups
self._expand_groups(self.groups) self._expand_groups()
else: else:
self.lights = [l.name for l in self.bridge.lights] self.lights = [l.name for l in self.bridge.lights]
logging.info('Configured lights: "{}"'. format(self.lights)) logging.info('Configured lights: "{}"'. format(self.lights))
def _expand_groups(self, group_names): def _expand_groups(self):
groups = [g for g in self.bridge.groups groups = [g for g in self.bridge.groups if g.name in self.groups]
if g.name in group_names]
for g in groups: for g in groups:
self.lights.extend([l.name for l in g.lights]) self.lights.extend([l.name for l in g.lights])

View File

@ -5,9 +5,18 @@ from platypush.message.response import Response
from .. import MusicPlugin from .. import MusicPlugin
class MusicMpdPlugin(MusicPlugin): class MusicMpdPlugin(MusicPlugin):
def _init(self): def __init__(self, host, port):
"""
Constructor
Params:
host -- MPD host
port -- MPD port
"""
self.host = host
self.port = port
self.client = mpd.MPDClient(use_unicode=True) self.client = mpd.MPDClient(use_unicode=True)
self.client.connect(self.config['host'], self.config['port']) self.client.connect(self.host, self.port)
def _exec(self, method, *args, **kwargs): def _exec(self, method, *args, **kwargs):
getattr(self.client, method)(*args, **kwargs) getattr(self.client, method)(*args, **kwargs)

View File

@ -7,7 +7,9 @@ from platypush.message.response import Response
from .. import SwitchPlugin from .. import SwitchPlugin
class SwitchWemoPlugin(SwitchPlugin): class SwitchWemoPlugin(SwitchPlugin):
def _init(self, discovery_seconds=3): def __init__(self, discovery_seconds=3):
super().__init__()
self.discovery_seconds=discovery_seconds self.discovery_seconds=discovery_seconds
self.env = Environment() self.env = Environment()
self.env.start() self.env.start()

View File

@ -2,7 +2,8 @@ import argparse
import re import re
import sys import sys
from platypush import init_backends, get_default_pusher_backend, parse_config_file from platypush.config import Config
from platypush.utils import init_backends
from platypush.message.request import Request from platypush.message.request import Request
def print_usage(): def print_usage():
@ -15,15 +16,16 @@ def print_usage():
'''.format(sys.argv[0])) '''.format(sys.argv[0]))
def pusher(target, action, backend=None, **kwargs): def pusher(target, action, backend=None, config=None, **kwargs):
config = parse_config_file() Config.init(config)
if target == 'localhost': if target == 'localhost':
backend = 'local' backend = 'local'
elif not backend: elif not backend:
backend = get_default_pusher_backend(config) backend = Config.get_default_pusher_backend()
backends = init_backends(config) # TODO Initialize a local bus and wait for the response
backends = init_backends()
if backend not in backends: if backend not in backends:
raise RuntimeError('No such backend configured: {}'.format(backend)) raise RuntimeError('No such backend configured: {}'.format(backend))
@ -37,6 +39,11 @@ def pusher(target, action, backend=None, **kwargs):
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--config', '-c', dest='config', required=False,
help="Configuration file path (default: " +
"~/.config/platypush/config.yaml or " +
"/etc/platypush/config.yaml")
parser.add_argument('--target', '-t', dest='target', required=True, parser.add_argument('--target', '-t', dest='target', required=True,
help="Destination of the command") help="Destination of the command")
@ -59,11 +66,14 @@ def main():
payload[re.sub('^-+', '', args[i])] = args[i+1] payload[re.sub('^-+', '', args[i])] = args[i+1]
pusher(target=opts.target, action=opts.action, pusher(target=opts.target, action=opts.action,
backend=opts.backend if 'backend' in opts else None, **payload) backend=opts.backend if 'backend' in opts else None,
config=opts.config if 'config' in opts else None,
**payload)
if __name__ == '__main__': if __name__ == '__main__':
main() main()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -0,0 +1,66 @@
import functools
import importlib
import logging
from platypush.config import Config
modules = {}
def get_or_load_plugin(plugin_name, reload=False):
global modules
if plugin_name in modules and not reload:
return modules[plugin_name]
try:
module = importlib.import_module('platypush.plugins.' + plugin_name)
except ModuleNotFoundError as e:
logging.warn('No such plugin: {}'.format(plugin_name))
raise RuntimeError(e)
# e.g. plugins.music.mpd main class: MusicMpdPlugin
cls_name = functools.reduce(
lambda a,b: a.title() + b.title(),
(plugin_name.title().split('.'))
) + 'Plugin'
plugin_conf = Config.get_plugins()[plugin_name] \
if plugin_name in Config.get_plugins() else {}
try:
plugin = getattr(module, cls_name)(**plugin_conf)
modules[plugin_name] = plugin
except AttributeError as e:
logging.warn('No such class in {}: {}'.format(
plugin_name, cls_name))
raise RuntimeError(e)
return plugin
def init_backends(bus=None):
backends = {}
for k in Config.get_backends().keys():
module = importlib.import_module('platypush.backend.' + k)
cfg = Config.get_backends()[k]
# e.g. backend.pushbullet main class: PushbulletBackend
cls_name = functools.reduce(
lambda a,b: a.title() + b.title(),
(module.__name__.title().split('.')[2:])
) + 'Backend'
try:
b = getattr(module, cls_name)(bus=bus, **cfg)
backends[k] = b
except AttributeError as e:
logging.warn('No such class in {}: {}'.format(
module.__name__, cls_name))
raise RuntimeError(e)
return backends
# vim:sw=4:ts=4:et: