def _worker(host, port): try: if exclude_local and (host == 'localhost' or host == Config.get('device_id')): return server_status = self.status(host=host, port=port).output client_status = self.status(host=host, port=port, client=Config.get('device_id')).output if client_status.get('config', {}).get('volume', {}).get('muted'): return group = [g for g in server_status.get('groups', {}) if g.get('id') == client_status.get('group_id')].pop(0) if group.get('muted'): return stream = [s for s in server_status.get('streams') if s.get('id') == group.get('stream_id')].pop(0) if stream.get('status') != 'playing': return playing_hosts[host] = port except Exception as e: self.logger.warning(('Error while retrieving the status of ' + 'Snapcast host at {}:{}: {}').format( host, port, str(e)))
def __init__(self, target=None, origin=None, id=None, timestamp=None, disable_logging=False, disable_web_clients_notification=False, **kwargs): """ Params: target -- Target node [String] origin -- Origin node (default: current node) [String] id -- Event ID (default: auto-generated) kwargs -- Additional arguments for the event [kwDict] """ super().__init__(timestamp=timestamp) self.id = id if id else self._generate_id() self.target = target if target else Config.get('device_id') self.origin = origin if origin else Config.get('device_id') self.type = '{}.{}'.format(self.__class__.__module__, self.__class__.__name__) self.args = kwargs self.disable_logging = disable_logging self.disable_web_clients_notification = disable_web_clients_notification for arg, value in self.args.items(): if arg != 'args': self.__setattr__(arg, value)
def __init__(self, config_file=None, backend=None, on_response=None): """ Constructor. Params: config_file -- Path to the configuration file - default: ~/.config/platypush/config.yaml or /etc/platypush/config.yaml) backend -- Name of the backend where pusher will send the request and wait for the response (kafka or pushbullet). Default: whatever is specified with pusher=true in your configuration file on_response -- Method that will be invoked upon response receipt. Takes a platypush.message.response.Response as arg. Default: print the response and exit. """ # Initialize the configuration self.config_file = config_file log_conf = Config.get('logging') Config.init(config_file) logging.basicConfig(level=log_conf['level'] if log_conf and 'level' in log_conf else logging.info, stream=sys.stdout) self.on_response = on_response or self.default_on_response() self.backend = backend or Config.get_default_pusher_backend() self.bus = Bus()
def __init__(self, bus=None, **kwargs): """ Params: bus -- Reference to the Platypush bus where the requests and the responses will be posted [Bus] kwargs -- key-value configuration for this backend [Dict] """ # If no bus is specified, create an internal queue where # the received messages will be pushed self.bus = bus or Bus() self.device_id = Config.get('device_id') self.thread_id = None self._stop = False self._kwargs = kwargs # Internal-only, we set the request context on a backend if that # backend is intended to react for a response to a specific request self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs \ else None Thread.__init__(self) logging.basicConfig( stream=sys.stdout, level=Config.get('logging') if 'logging' not in kwargs else getattr(logging, kwargs['logging']))
def stop(self): """ Stops the bus by sending a STOP event """ evt = StopEvent(target=Config.get('device_id'), origin=Config.get('device_id'), thread_id=self.thread_id) self.post(evt)
def plugin_route(plugin): """ Route to the plugin pane template """ js_folder = os.path.abspath(os.path.join(template_folder, '..', 'static', 'js')) style_folder = os.path.abspath(os.path.join(template_folder, '..', 'static', 'css', 'dist')) template_file = os.path.join(template_folder, 'plugins', plugin, 'index.html') script_file = os.path.join(js_folder, 'plugins', plugin, 'index.js') style_file = os.path.join(style_folder, 'webpanel', 'plugins', plugin+'.css') conf = Config.get(plugin) or {} if os.path.isfile(template_file): conf['_template_file'] = '/' + '/'.join(template_file.split(os.sep)[-3:]) if os.path.isfile(script_file): conf['_script_file'] = '/'.join(script_file.split(os.sep)[-4:]) if os.path.isfile(style_file): conf['_style_file'] = 'css/dist/' + style_file[len(style_folder)+1:] http_conf = Config.get('backend.http') return render_template('plugin.html', plugin=plugin, conf=conf, template=conf.get('_template_file', {}), script=conf.get('_script_file', {}), style=conf.get('_style_file', {}), utils=HttpUtils, token=Config.get('token'), websocket_port=get_websocket_port(), template_folder=template_folder, static_folder=static_folder, has_ssl=http_conf.get('ssl_cert') is not None)
def get_manifests_from_conf( conf_file: Optional[str] = None) -> Mapping[str, Manifest]: import platypush from platypush.config import Config conf_args = [] if conf_file: conf_args.append(conf_file) Config.init(*conf_args) app_dir = os.path.dirname(inspect.getfile(platypush)) manifest_files = set() for name in Config.get_backends().keys(): manifest_files.add( os.path.join(app_dir, 'backend', *name.split('.'), 'manifest.yaml')) for name in Config.get_plugins().keys(): manifest_files.add( os.path.join(app_dir, 'plugins', *name.split('.'), 'manifest.yaml')) return { manifest_file: Manifest.from_file(manifest_file) for manifest_file in manifest_files }
def build(args): global workdir ports = set() parser = argparse.ArgumentParser( prog='platydock build', description='Build a Platypush image from a config.yaml') parser.add_argument('-c', '--config', type=str, required=True, help='Path to the platypush configuration file') parser.add_argument('-p', '--python-version', type=str, default='3.9', help='Python version to be used') opts, args = parser.parse_known_args(args) cfgfile = os.path.abspath(os.path.expanduser(opts.config)) manifest._available_package_manager = 'apt' # Force apt for Debian-based Docker images install_cmds = manifest.get_dependencies_from_conf(cfgfile) python_version = opts.python_version backend_config = Config.get_backends() # Container exposed ports if backend_config.get('http'): from platypush.backend.http import HttpBackend # noinspection PyProtectedMember ports.add(backend_config['http'].get('port', HttpBackend._DEFAULT_HTTP_PORT)) # noinspection PyProtectedMember ports.add(backend_config['http'].get( 'websocket_port', HttpBackend._DEFAULT_WEBSOCKET_PORT)) if backend_config.get('tcp'): ports.add(backend_config['tcp']['port']) if backend_config.get('websocket'): from platypush.backend.websocket import WebsocketBackend # noinspection PyProtectedMember ports.add(backend_config['websocket'].get( 'port', WebsocketBackend._default_websocket_port)) dev_dir = os.path.join(workdir, Config.get('device_id')) generate_dockerfile(deps=dict(install_cmds), ports=ports, cfgfile=cfgfile, device_dir=dev_dir, python_version=python_version) subprocess.call([ 'docker', 'build', '-t', 'platypush-{}'.format(Config.get('device_id')), dev_dir ])
def get_config(self, entry: Optional[str] = None) -> dict: """ Return the configuration of the application or of a section. :param entry: [Optional] configuration entry name to retrieve (e.g. ``workdir`` or ``backend.http``). :return: The requested configuration object. """ if entry: return Config.get(entry) cfg = Config.get() return cfg
def build(cls, action): action = super().parse(action) action['origin'] = Config.get('device_id') if 'target' not in action: action['target'] = action['origin'] token = Config.get('token') if token: action['token'] = token return super().build(action)
def dashboard(): """ Route for the fullscreen dashboard """ http_conf = Config.get('backend.http') dashboard_conf = http_conf.get('dashboard', {}) return render_template('dashboard.html', config=dashboard_conf, utils=HttpUtils, token=Config.get('token'), static_folder=static_folder, template_folder=template_folder, websocket_port=get_websocket_port(), has_ssl=http_conf.get('ssl_cert') is not None)
def __init__(self, target=None, origin=None, id=None, **kwargs): """ Params: target -- Target node [String] origin -- Origin node (default: current node) [String] id -- Event ID (default: auto-generated) kwargs -- Additional arguments for the event [kwDict] """ self.id = id if id else self._generate_id() self.target = target if target else Config.get('device_id') self.origin = origin if origin else Config.get('device_id') self.type = '{}.{}'.format(self.__class__.__module__, self.__class__.__name__) self.args = kwargs
def build(cls, msg): """ Builds an event message from a JSON UTF-8 string/bytearray, a dictionary, or another Event """ msg = super().parse(msg) event_type = msg['args'].pop('type') event_class = get_event_class_by_type(event_type) args = msg['args'] if 'args' in msg else {} args['id'] = msg['id'] if 'id' in msg else cls._generate_id() args['target'] = msg['target'] if 'target' in msg else Config.get( 'device_id') args['origin'] = msg['origin'] if 'origin' in msg else Config.get( 'device_id') return event_class(**args)
def __init__(self, url, title=None, headers=None, params=None, max_entries=None, extract_content=False, digest_format=None, *argv, **kwargs): self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'feeds') self.dbfile = os.path.join(self.workdir, 'rss.db') self.url = url self.title = title self.max_entries = max_entries # If true, then the http.webpage plugin will be used to parse the content self.extract_content = extract_content self.digest_format = digest_format.lower() if digest_format else None # Supported formats: html, pdf os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True) if headers is None: headers = {} headers['User-Agent'] = self.user_agent request_args = { 'method': 'get', 'url': self.url, 'headers': headers, 'params': params or {}, } super().__init__(skip_first_call=False, args=request_args, *argv, **kwargs)
def __init__(self, url, title=None, headers=None, params=None, max_entries=None, extract_content=False, digest_format=None, user_agent: str = user_agent, body_style: str = 'font-size: 22px; ' + 'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;', title_style: str = 'margin-top: 30px', subtitle_style: str = 'margin-top: 10px; page-break-after: always', article_title_style: str = 'page-break-before: always', article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted', article_content_style: str = '', *argv, **kwargs): """ :param url: URL to the RSS feed to be monitored. :param title: Optional title for the feed. :param headers: Extra headers to be passed to the request. :param params: Extra GET parameters to be appended to the URL. :param max_entries: Maximum number of entries that will be returned in a single :class:`platypush.message.event.http.rss.NewFeedEvent` event. :param extract_content: Whether the context should also be extracted (through the :class:`platypush.plugins.http.webpage.HttpWebpagePlugin` plugin) (default: ``False``). :param digest_format: Format of the digest output file (default: None, text. Other supported types: ``html`` and ``pdf`` (requires the ``weasyprint`` module installed). :param user_agent: User agent string to be passed on the request. :param body_style: CSS style for the body. :param title_style: CSS style for the feed title. :param subtitle_style: CSS style for the feed subtitle. :param article_title_style: CSS style for the article titles. :param article_link_style: CSS style for the article link. :param article_content_style: CSS style for the article content. """ self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'feeds') self.dbfile = os.path.join(self.workdir, 'rss.db') self.url = url self.title = title self.max_entries = max_entries self.user_agent = user_agent self.body_style = body_style self.title_style = title_style self.subtitle_style = subtitle_style self.article_title_style = article_title_style self.article_link_style = article_link_style self.article_content_style = article_content_style # If true, then the http.webpage plugin will be used to parse the content self.extract_content = extract_content self.digest_format = digest_format.lower() if digest_format else None # Supported formats: html, pdf os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True) if headers is None: headers = {} headers['User-Agent'] = self.user_agent request_args = { 'method': 'get', 'url': self.url, 'headers': headers, 'params': params or {}, } super().__init__(skip_first_call=False, args=request_args, *argv, **kwargs)
def resources_path(path): """ Custom static resources """ path_tokens = path.split('/') filename = path_tokens.pop(-1) http_conf = Config.get('backend.http') resource_dirs = http_conf.get('resource_dirs', {}) while path_tokens: if '/'.join(path_tokens) in resource_dirs: break path_tokens.pop() if not path_tokens: # Requested resource not found in the allowed resource_dirs abort(404) base_path = '/'.join(path_tokens) real_base_path = os.path.abspath(os.path.expanduser(resource_dirs[base_path])) real_path = real_base_path file_path = [s for s in re.sub(r'^{}(.*)$'.format(base_path), '\\1', path) .split('/') if s] for p in file_path[:-1]: real_path += os.sep + p file_path.pop(0) file_path = file_path.pop(0) if not real_path.startswith(real_base_path): # Directory climbing attempt abort(404) return send_from_directory(real_path, file_path)
def get_or_create_ngrok_tunnel() -> str: """ This method creates an ngrok tunnel for the local web application, useful to register public HTTPS callback URLs on the fly from plugins and backends. """ global _app_tunnel_url with _app_tunnel_lock: if _app_tunnel_url: return _app_tunnel_url local_port = _get_http_port() ngrok = None if Config.get('ngrok'): ngrok = get_plugin('ngrok') assert ngrok, 'The ngrok plugin is required in order to subscribe to notifications' tunnel_response = ngrok.create_tunnel( resource=local_port, protocol='http', bind_tls=True, ).output _app_tunnel_url = tunnel_response.get('url') assert _app_tunnel_url, 'Unable to create an ngrok tunnel' return _app_tunnel_url
def _get_http_port() -> int: http = None if Config.get('backend.http'): http = get_backend('http') assert http, 'The http backend is required in order to subscribe to notifications' return http.port
def _execute_procedure(self, *args, **kwargs): from platypush.config import Config from platypush.procedure import Procedure logger.info('Executing procedure request: {}'.format(self.action)) procedures = Config.get_procedures() proc_name = '.'.join(self.action.split('.')[1:]) if proc_name not in procedures: proc_name = self.action.split('.')[-1] proc_config = procedures[proc_name] if is_functional_procedure(proc_config): kwargs.update(**self.args) if 'n_tries' in kwargs: del kwargs['n_tries'] return proc_config(*args, **kwargs) proc = Procedure.build(name=proc_name, requests=proc_config['actions'], _async=proc_config['_async'], args=self.args, backend=self.backend, id=self.id) return proc.execute(*args, **kwargs)
def __init__(self, host: Optional[str] = None, port: int = 8002, timeout: Optional[int] = 5, name='platypush', token_file: Optional[str] = None, **kwargs): """ :param host: IP address or host name of the smart TV. :param port: Websocket port (default: 8002). :param timeout: Connection timeout in seconds (default: 5, specify 0 or None for no timeout). :param name: Name of the remote device (default: platypush). :param token_file: Path to the token file (default: ``~/.local/share/platypush/samsungtvws/token.txt``) """ super().__init__(**kwargs) self.workdir = os.path.join(Config.get('workdir'), 'samsungtvws') if not token_file: token_file = os.path.join(self.workdir, 'token.txt') self.host = host self.port = port self.timeout = timeout self.name = name self.token_file = token_file self._connections: Dict[Tuple[host, port], SamsungTVWS] = {} os.makedirs(self.workdir, mode=0o700, exist_ok=True)
def __init__(self, bus=None, **kwargs): """ :param bus: Reference to the bus object to be used in the backend :type bus: platypush.bus.Bus :param kwargs: Key-value configuration for the backend :type kwargs: dict """ self._thread_name = self.__class__.__name__ EventGenerator.__init__(self) Thread.__init__(self, name=self._thread_name) # If no bus is specified, create an internal queue where # the received messages will be pushed self.bus = bus or Bus() self.device_id = Config.get('device_id') self.thread_id = None self._stop = False self._kwargs = kwargs self.logger = logging.getLogger(self.__class__.__name__) # Internal-only, we set the request context on a backend if that # backend is intended to react for a response to a specific request self._request_context = kwargs['_req_ctx'] if '_req_ctx' in kwargs \ else None if 'logging' in kwargs: self.logger.setLevel( getattr(logging, kwargs.get('logging').upper())) Thread.__init__(self)
def __init__(self, dirs, *args, **kwargs): super().__init__() self.dirs = dirs db_dir = os.path.join(Config.get('workdir'), 'media') os.makedirs(db_dir, exist_ok=True) self.db_file = os.path.join(db_dir, 'media.db') self._db_engine = None
def search_web_directory(cls, directory, *extensions): directory = os.path.abspath(os.path.expanduser(directory)) resource_dirs = (Config.get('backend.http') or {}).get('resource_dirs', {}) resource_path = None uri = '' for name, resource_path in resource_dirs.items(): resource_path = os.path.abspath(os.path.expanduser(resource_path)) if directory.startswith(resource_path): subdir = re.sub('^{}(.*)$'.format(resource_path), '\\1', directory) uri = '/resources/' + name break if not uri: raise RuntimeError( ('Directory {} not found among the available ' + 'static resources on the webserver').format(directory)) results = [ re.sub('^{}(.*)$'.format(resource_path), uri + '\\1', path) for path in cls.search_directory(directory, *extensions) ] return results
def __init__(self, device: str, config_path: Optional[str] = None, user_path: Optional[str] = None, ready_timeout: float = 10.0, *args, **kwargs): """ :param device: Path to the Z-Wave adapter (e.g. /dev/ttyUSB0 or /dev/ttyACM0). :param config_path: Z-Wave configuration path (default: ``<OPENZWAVE_PATH>/ozw_config``). :param user_path: Z-Wave user path where runtime and configuration files will be stored (default: ``<PLATYPUSH_WORKDIR>/zwave``). :param ready_timeout: Network ready timeout in seconds (default: 60). """ import python_openzwave from openzwave.network import ZWaveNetwork super().__init__(*args, **kwargs) self.device = device if not config_path: config_path = os.path.join( os.path.dirname(inspect.getfile(python_openzwave)), 'ozw_config') if not user_path: user_path = os.path.join(Config.get('workdir'), 'zwave') os.makedirs(user_path, mode=0o770, exist_ok=True) self.config_path = config_path self.user_path = user_path self.ready_timeout = ready_timeout self.network: Optional[ZWaveNetwork] = None
def _expand_context(self, event_args=None, **context): from platypush.config import Config if event_args is None: event_args = copy.deepcopy(self.args) constants = Config.get_constants() context['constants'] = {} for (name,value) in constants.items(): context['constants'][name] = value keys = [] if isinstance(event_args, dict): keys = event_args.keys() elif isinstance(event_args, list): keys = range(0, len(event_args)) for key in keys: value = event_args[key] if isinstance(value, str): value = self.expand_value_from_context(value, **context) elif isinstance(value, dict) or isinstance(value, list): self._expand_context(event_args=value, **context) event_args[key] = value return event_args
def index(): """ Route to the main web panel """ configured_plugins = Config.get_plugins() enabled_templates = {} enabled_scripts = {} enabled_styles = {} js_folder = os.path.abspath( os.path.join(template_folder, '..', 'static', 'js')) style_folder = os.path.abspath( os.path.join(template_folder, '..', 'static', 'css', 'dist')) for plugin, conf in configured_plugins.items(): template_file = os.path.join(template_folder, 'plugins', plugin, 'index.html') script_file = os.path.join(js_folder, 'plugins', plugin, 'index.js') style_file = os.path.join(style_folder, 'webpanel', 'plugins', plugin + '.css') if os.path.isfile(template_file): conf['_template_file'] = '/' + '/'.join( template_file.split(os.sep)[-3:]) enabled_templates[plugin] = conf if os.path.isfile(script_file): conf['_script_file'] = '/'.join(script_file.split(os.sep)[-4:]) enabled_scripts[plugin] = conf if os.path.isfile(style_file): conf['_style_file'] = 'css/dist/' + style_file[len(style_folder) + 1:] enabled_styles[plugin] = conf http_conf = Config.get('backend.http') return render_template('index.html', templates=enabled_templates, scripts=enabled_scripts, styles=enabled_styles, utils=HttpUtils, token=Config.get('token'), websocket_port=get_websocket_port(), template_folder=template_folder, static_folder=static_folder, plugins=Config.get_plugins(), backends=Config.get_backends(), has_ssl=http_conf.get('ssl_cert') is not None)
def send_message(msg, wait_for_response=True): msg = Message.build(msg) if isinstance(msg, Request): msg.origin = 'http' if Config.get('token'): msg.token = Config.get('token') bus().post(msg) if isinstance(msg, Request) and wait_for_response: response = get_message_response(msg) logger().debug( 'Processing response on the HTTP backend: {}'.format(response)) return response
def get_credentials_filename(*scopes): from platypush.config import Config scope_name = '-'.join([scope.split('/')[-1] for scope in scopes]) credentials_dir = os.path.join(Config.get('workdir'), 'credentials', 'google') os.makedirs(credentials_dir, exist_ok=True) return os.path.join(credentials_dir, scope_name + '.json')
def build(cls, msg): msg = super().parse(msg) args = {'target': msg.get('target', Config.get('device_id')), 'action': msg['action'], 'args': msg.get('args', {}), 'id': msg['id'] if 'id' in msg else cls._generate_id(), 'timestamp': msg['_timestamp'] if '_timestamp' in msg else time.time()} if 'origin' in msg: args['origin'] = msg['origin'] if 'token' in msg: args['token'] = msg['token'] return cls(**args)
def get_switch_plugins(self) -> dict: """ :return: The list of enabled switch plugins as a ``name -> configuration`` map. """ from platypush.plugins.switch import SwitchPlugin return { name: Config.get(name) for name, plugin in get_enabled_plugins().items() if isinstance(plugin, SwitchPlugin) }