How to extend Medulla First of all, there are a few important things to note when developing around the agent: The machine agent is automatically updated when the code is not identical between the agent and the agent codebase that is on the server. To disable this behaviour, the following setting needs to be added to agentconf.ini and the agent restarted: [updateagent] updating = 0 The plugins are also automatically updated if the version has changed. If a plugin needs to be modified, do not update the version until all the tests have been done. The are four types of plugins for the machine agent: start plugins, update plugins, action plugins and scheduled plugins. Start plugins are those run when the agent start and are defined in start_machine.ini; Update plugins are used to install or update external components used by the agent; Action plugins are called by an action received by the agent; Scheduled plugins are those called at a specific time or interval. Each plugin can have its own config file named after the plugin name and need to be added to the following parameter in agentconf.ini for the configuration to be loaded: [plugin] pluginlist = xxxxxxx, yyyyyyy The scheduled plugins have their own schedule defined in the plugin in the SCHEDULE parameter. This however can be overridden in the manage_scheduler_machine.ini file The are 3 ways to extend Medulla: interaction with the machine agent via a TCP socket interaction with the machine agent via named pipes machine agent action plugins machine agent scheduled plugins How to interact with the agent via a TCP socket A new action needs to be defined in  server_kiosk.py  in the function named  handle_client_connection  in the condition  if 'action' in result: and this action added to the JSON message that is sent on the TCP socket.  Here is an example of the message sent: { "action": "myNewAction", "sessionid": "mysessionid", "base64": false, "data": { "date": "2020-06-24T15:45:02.000Z", "family1": { "field1": "value1", "field2": "value2" }, "family2": { "field1": "value1", "field2": "value2" } } } The above content is to be saved to a file named  json_file  to be sent via TCP socket or added to a variable named  json_message  in your code for sending via named pipes And its counterpart in  handle_client_connection  function in  manage_kiosk_message  class. In the following section: try: _result = json.loads(minifyjsonstringrecv(msg)) Add if _result['action'] == "myNewAction": substitute_recv = self.objectxmpp.sub_monitoring logging.getLogger().warning("send to %s to %s" % (_result,substitute_recv )) self.objectxmpp.send_message(mbody=json.dumps(_result), mto=substitute_recv, mtype='chat') return And in the following loop: if 'action' in result: if result['action'] == "kioskinterface": ... Add elif result['action'] == "myNewAction": datasend['action'] = "myNewSubstituteAction" subs_recv = self.objectxmpp.sub_monitoring datasend['sessionid'] = getRandomName(6, "mynewsubstituteaction") datasend['data'] = result['data'] The above example will in turn send the message to sub_monitoring jid with a new action to be carried out: myNewSubstituteAction The listening TCP port is defined in  agentconf.ini  parameter  kiosk/am_local_port . Default value is 8765. Here is an example TCP sender written in python for the next step which is the actual sending of the data to the TCP socket:  #!/usr/bin/env python # -*- coding: utf-8; -*- # # (c) 2023 siveo, http://www.siveo.net # # This file is part of Medulla, http://www.siveo.net # # Pulse 2 is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # Pulse 2 is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Medulla; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # file clientTCPcli.py # Run python clientTCPcli.py -p ./file.json on the client machine to # inject the data from optparse import OptionParser import socket import sys import os import select def send_message(message, host, port, timeout_in_seconds = 5): # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Connect the socket to the port where the server is listening try: server_address = (host, port) print >>sys.stderr, 'connecting to %s port %s' % server_address sock.connect(server_address) except socket.error , msgerror: print 'Bind failed on command interface ' + host + ' port ' + str(port) + ' Error Code : ' + str(msgerror[0]) + ' Message ' + msgerror[1] + '\n' sys.exit(str(msgerror[0])) try: # Send data print >>sys.stderr, 'sending "%s"' % message sock.sendall(message) # Look for the response amount_received = 0 amount_expected = len(message) ready = select.select([sock], [], [], timeout_in_seconds) data = "" if ready[0]: data = sock.recv(4096) return 0, data return -1,"" finally: print >>sys.stderr, 'closing socket' sock.close() if __name__ == '__main__': optp = OptionParser() #optp.add_option("-h", "--help",action="store_true", #dest="help", default=False, #help="host") optp.add_option("-H", "--host",action="store_true", dest="host", default="localhost", help="host") optp.add_option("-P", "--port", dest="port", default=8765, help="port connection") optp.add_option("-T", "--timeout", dest="timeout_in_seconds", default=2, help="Revc timeout in seconds") optp.add_option("-m", "--msg", dest="msg", default = "", help="message for sending to TCP server") optp.add_option("-p", "--pathfile", dest="pathfile", default = None, help="content File for sending to TCP server") opts, args = optp.parse_args() #if opts.help: #print "usage commande" #os._exit(0) message = "" if opts.pathfile is None: if opts.msg =="": print "option missing < -m | -p> " sys.exit(-1) message = opts.msg else: if os.path.exists(opts.pathfile): with open(opts.pathfile, 'r') as f: message = f.read() if message !="": code, msg = send_message(message, opts.host, opts.port, opts.timeout_in_seconds) print "code error %s, reponse server %s"%(code,msg) To send the message:  python clientTCPcli.py -p How to interact with the agent via named pipes The way to interact with the agent via named pipes is done the same way as for interacting with the agent via TCP socket except for sending the message. To send the message to the named pipe here is an example code written in Python: import win32file def send_message(json_message): fileHandle = win32file.CreateFile("\\\\.\\pipe\\interfacechang", win32file.GENERIC_READ | win32file.GENERIC_WRITE, 0, None, win32file.OPEN_EXISTING, 0, None) win32file.WriteFile(fileHandle, json_message) win32file.CloseHandle(fileHandle) How to write action plugins for the agent Below is a template that can be used for writing action plugins: import logging import json plugin = {"VERSION": "1.0", "NAME": "mynewaction", "TYPE": "machine"} logger = logging.getLogger() def action( objectxmpp, action, sessionid, data, message, dataerreur): logger.debug("###################################################") logger.debug("call %s from %s session id %s" % (plugin, message['from'], sessionid)) logger.debug("###################################################") datasend = {"action" : "myNewSubstituteAction", "data" : data, "sessionid": sessionid, "ret": 0, "base64": False } objectxmpp.send_message(mto=objectxmpp.sub_monitoring, mbody=json.dumps(datasend), mtype='chat') Please note the following:  NAME must match the name of the plugin file. Here the file will be named plugin_mynewaction.py TYPE must be defined to machine, relayserver or all depending on its target the action function will be the code executed by default. The above example will in turn send the message to sub_monitoring jid with a new action to be carried out: myNewSubstituteAction How to write scheduled plugins for the agent Below is a template that can be used for writing scheduled plugins: import logging import json import os import ConfigParser from pulse_xmpp_agent.lib.agentconffile import directoryconffile from pulse_xmpp_agent.lib.utils import file_put_contents plugin = {"VERSION": "1.0", "NAME": "scheduling_mynewscheduledaction", "TYPE": "machine", "SCHEDULED" : True} SCHEDULE = {"schedule" : "*/15 * * * *", "nb" : -1} logger = logging.getLogger() def schedule_main(xmppobject): logger.debug("===================================================") logger.debug(plugin) logger.debug("===================================================") if xmppobject.num_call_scheduling_mynewscheduledaction == 0: __read_conf(xmppobject) if xmppobject.config.mynewscheduledaction_enable: data = {} data['family1'] = {} data['family1']['field1'] = "value1" data['family1']['field2'] = "value2" data['family2'] = {} data['family2']['field1'] = "value1" data['family2']['field2'] = "value2" if xmppobject.config.mynewscheduledaction_forward: datasend = {"action" : "myNewSubstituteAction", "data" : data, "sessionid": "mysessionid", "base64": False } objectxmpp.send_message(mto=objectxmpp.sub_monitoring, mbody=json.dumps(datasend), mtype='chat') def __read_conf(xmppobject): """ Read the plugin configuration """ configfilename = os.path.join(directoryconffile(), "%s.ini" % plugin['NAME']) logger.debug("Reading configuration in file %s" % configfilename) #default parameters xmppobject.config.mynewscheduledaction_enable = True xmppobject.config.mynewscheduledaction_forward = False if not os.path.isfile(configfilename): logger.warning("Plugin %s configuration file %s missing" % (plugin['NAME'], configfilename)) logger.warning("The missing configuration file will be created automatically.") file_put_contents(configfilename, "[mynewscheduledaction]\n" \ "enable = 1\n" \ "forward = 0\n") # Load configuration from file Config = ConfigParser.ConfigParser() Config.read(configfilename) if os.path.exists(configfilename + ".local"): Config.read(configfilename + ".local") if Config.has_section("mynewscheduledaction"): if Config.has_option("mynewscheduledaction", "enable"): xmppobject.config.mynewscheduledaction_enable = Config.getboolean('mynewscheduledaction','enable') if Config.has_option("mynewscheduledaction", "forward"): xmppobject.config.mynewscheduledaction_forward = Config.getboolean('mynewscheduledaction','forward') Please note the following:  NAME must match the name of the plugin file. Here the file will be named plugin_mynewaction.py TYPE must be defined to machine, relayserver or all depending on its target SCHEDULE notation is similar to the cron notation. The extra parameter nb defines how many times the plugin must run. If -1, it will run forever the schedule_main function will be the code executed by default. The above example will read a config file or create it if it does not exist and send a message to sub_monitoring jid with a new action to be carried out: myNewSubstituteAction