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 socket
import sys
import traceback
import yaml
from queue import Queue
from threading import Thread
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.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):
tokens = request.action.split('.')
module_name = str.join('.', tokens[:-1])
method_name = tokens[-1:][0]
try:
plugin = _init_plugin(module_name)
plugin = get_or_load_plugin(module_name)
except RuntimeError as e: # Module/class not found
logging.exception(e)
return
@ -78,7 +38,7 @@ def _execute_request(request, retry=True):
logging.exception(e)
if retry:
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)
finally:
# Send the response on the backend that received the request
@ -95,131 +55,32 @@ def on_msg(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():
print('Starting platypush v.{}'.format(__version__))
debug = False
config_file = None
plugins_dir = os.path.join(wrkdir, 'plugins')
sys.path.insert(0, plugins_dir)
optlist, args = getopt(sys.argv[1:], 'vh')
for opt, arg in optlist:
if opt == '-c':
config_file = arg
if opt == '-v':
debug = True
elif opt == '-h':
print('''
Usage: {} [-v] [-h] [-c <config_file>]
-v Enable debug mode
Usage: {} [-h] [-c <config_file>]
-h Show this help
-c Path to the configuration file (default: ./config.yaml)
'''.format(sys.argv[0]))
return
config = parse_config_file(config_file)
if debug: config['logging'] = logging.DEBUG
Config.init(config_file)
logging.basicConfig(level=Config.get('logging'), stream=sys.stdout)
logging.basicConfig(level=get_logging_level(), stream=sys.stdout)
logging.debug('Configuration dump: {}'.format(config))
bus = Queue()
backends = init_backends(config, bus)
bus = Bus(on_msg=on_msg)
backends = init_backends(bus)
for backend in backends.values():
backend.start()
while True:
try:
on_msg(bus.get())
except KeyboardInterrupt:
return
bus.loop_forever()
if __name__ == '__main__':
main()

View file

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

View file

@ -4,6 +4,7 @@ import requests
import time
import websocket
from platypush.config import Config
from platypush.message import Message
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 = {
'target' : msg['target'],
'action' : msg['action'],
'args' : msg['args'],
'args' : msg['args'] if 'args' in msg else {},
}
if 'origin' in msg: args['origin'] = msg['origin']

View file

@ -1,23 +1,16 @@
import os
import sys
import logging
from platypush import get_logging_level
from platypush.config import Config
from platypush.message.response import Response
class Plugin(object):
def __init__(self, config):
self.config = config
logging.basicConfig(stream=sys.stdout, level=get_logging_level()
if 'logging' not in config
else getattr(logging, config.pop('logging')))
""" Base plugin class """
for cls in reversed(self.__class__.mro()):
if cls is not object:
try:
cls._init(self)
except AttributeError as e:
pass
def __init__(self, **kwargs):
logging.basicConfig(stream=sys.stdout, level=Config.get('logging')
if 'logging' not in kwargs
else getattr(logging, kwargs['logging']))
def run(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
class LightHuePlugin(LightPlugin):
""" Philips Hue lights plugin """
MAX_BRI = 255
MAX_SAT = 255
MAX_HUE = 65535
def _init(self):
self.bridge_address = self.config['bridge']
def __init__(self, bridge, lights=None, groups=None):
"""
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
logging.info('Initializing Hue lights plugin - bridge: "{}"'.
format(self.bridge_address))
@ -20,20 +32,18 @@ class LightHuePlugin(LightPlugin):
self.connect()
self.lights = []; self.groups = []
if 'lights' in self.config:
self.lights = self.config['lights']
elif 'groups' in self.config:
self.groups = self.config['groups']
self._expand_groups(self.groups)
if lights:
self.lights = lights
elif groups:
self.groups = groups
self._expand_groups()
else:
self.lights = [l.name for l in self.bridge.lights]
logging.info('Configured lights: "{}"'. format(self.lights))
def _expand_groups(self, group_names):
groups = [g for g in self.bridge.groups
if g.name in group_names]
def _expand_groups(self):
groups = [g for g in self.bridge.groups if g.name in self.groups]
for g in groups:
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
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.connect(self.config['host'], self.config['port'])
self.client.connect(self.host, self.port)
def _exec(self, method, *args, **kwargs):
getattr(self.client, method)(*args, **kwargs)

View file

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

View file

@ -2,7 +2,8 @@ import argparse
import re
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
def print_usage():
@ -15,15 +16,16 @@ def print_usage():
'''.format(sys.argv[0]))
def pusher(target, action, backend=None, **kwargs):
config = parse_config_file()
def pusher(target, action, backend=None, config=None, **kwargs):
Config.init(config)
if target == 'localhost':
backend = 'local'
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:
raise RuntimeError('No such backend configured: {}'.format(backend))
@ -37,6 +39,11 @@ def pusher(target, action, backend=None, **kwargs):
def main():
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,
help="Destination of the command")
@ -59,11 +66,14 @@ def main():
payload[re.sub('^-+', '', args[i])] = args[i+1]
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__':
main()
# 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: