TouchPortalAPI v1.7.8

TouchPortalAPI.client

  1__copyright__ = """
  2    This file is part of the TouchPortal-API project.
  3    Copyright (c) TouchPortal-API Developers/Contributors
  4    Copyright (C) 2021 DamienS
  5    All rights reserved.
  6
  7    This program is free software: you can redistribute it and/or modify
  8    it under the terms of the GNU General Public License as published by
  9    the Free Software Foundation, either version 3 of the License, or
 10    (at your option) any later version.
 11
 12    This program is distributed in the hope that it will be useful,
 13    but WITHOUT ANY WARRANTY; without even the implied warranty of
 14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15    GNU General Public License for more details.
 16
 17    You should have received a copy of the GNU General Public License
 18    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 19"""
 20
 21import socket
 22import selectors
 23import json
 24from pyee import ExecutorEventEmitter
 25from concurrent.futures import Executor, ThreadPoolExecutor
 26from threading import Event, Lock
 27from .logger import Logger
 28from .tools import Tools
 29from typing import TextIO
 30import sys
 31
 32__all__ = ['Client', 'TYPES']
 33
 34class TYPES:
 35    """
 36    Event type enumerations. These correspond to the message types Touch Portal may send.
 37    Event handler callbacks receive one parameter, which is the JSON data structure sent
 38    from TP and then converted to a corresponding Python object using `json.loads()`.
 39    Usage example:
 40        ```py
 41        import TouchPortalAPI as TP
 42
 43        client = TP.Client("myplugin")
 44
 45        @client.on(TP.TYPES.onConnect)
 46        # equivalent to: @client.on('info')
 47        def onConnect(data):
 48            print(data)
 49
 50        # alternatively, w/out decorators:
 51        def onAction(data):
 52            print(data)
 53
 54        client.on(TP.TYPES.onAction, onAction)
 55        ```
 56    """
 57    onHold_up = 'up'
 58    """ When the action is used in a hold functionality and the user releases the Touch Portal button (Api 3.0+) """
 59    onHold_down = 'down'
 60    """ When the action is used in a hold functionality and the user presses the Touch Portal button down (Api 3.0+) """
 61    onConnect = 'info'
 62    """ Info message returned after successful pairing. """
 63    onAction = 'action'
 64    """ When actions are triggered by the user (Api 1.0+) """
 65    onListChange = 'listChange'
 66    """ When a user makes a change in one of your lists (Api 1.0+) """
 67    onConnectorChange = 'connectorChange'
 68    """ When a connector is used in a slider functionality (connector) (Api 4.0+) """
 69    onShutdown = 'closePlugin'
 70    """ When Touch Portal tries to close your plug-in (Api 2.0+) """
 71    onBroadcast = 'broadcast'
 72    """ When Touch Portal broadcasts a message (Api 3.0+) """
 73    onSettingUpdate = 'settings'
 74    """ When the plugin's Settings have been updated (by user or from the plugin itself) (Api 3.0+) """
 75    onNotificationOptionClicked = "notificationOptionClicked"
 76    """ When a user clicks on a notification action (Api 4.0+) """
 77    shortConnectorIdNotification = 'shortConnectorIdNotification'
 78    """ When creating new connector for the first time It will generate shortid and It will send as an event (Api 5.0+) """
 79    allMessage = 'any'
 80    """ Special event handler which will receive **all** messages from TouchPortal. """
 81    onError = 'error'
 82    """ Special event emitted when any other event callback raises an exception.
 83        For this particular event, the parameter passed to the callback handler will be an `Exception` object.
 84        See also `pyee.ExecutorEventEmitter.error` event.
 85    """
 86
 87class Client(ExecutorEventEmitter):
 88    """
 89    A TCP/IP client for [Touch Portal API](https://www.touch-portal.com/api) plugin integration.
 90    Implements a [pyee.ExecutorEventEmitter](https://pyee.readthedocs.io/en/latest/#pyee.ExecutorEventEmitter).
 91
 92    After an initial connection to a Touch Portal desktop application "server," the client
 93    implements a send/receive event loop while maintaining the open sockets. Messages between
 94    TP and the plugin are exchanged asynchronously, with all sending methods possibly returning
 95    before the actual data is sent.
 96
 97    Messages from Touch Portal are delivered to the plugin script via event handler callbacks,
 98    which can be either individual callbacks per message type, and/or a single handler for all
 99    types of messages. The callbacks are executed in separate Thread(s), using a pool of up to
100    any number of concurrent threads as needed (if using more than one thread, the plugin
101    code itself is responsible for thread safety of its internal data). Please check the
102    `pyee.EventEmitter` documentation for general information on how events are handled
103    (which can be either via function decorators or via the inherited `Client.on()` method).
104
105    Generated event names correspond to Touch Portal API message types, one for each type
106    as defined in the TP API documentation (eg. "action" or "listChange") as well as one event
107    which is generated for all message types ("any"). Alternately, for a more formal approach,
108    the corresponding members of the `TYPES` class could be used instead of string names
109    (eg. `TYPES.onAction` or `TYPES.onListChange`).
110
111    Any errors raised within event listeners/handlers are trapped and then reported in the
112    inherited "error" event (from pyee.ExecutorEventEmitter), aka `TYPES.onError`.
113    """
114    TPHOST = '127.0.0.1'
115    """ TP plugin server host IPv4 address. """
116    TPPORT = 12136
117    """ TP plugin server host IPv4 port number. """
118    RCV_BUFFER_SZ = 4096
119    """ [B] incoming data buffer size. """
120    SND_BUFFER_SZ = 32**4
121    """ [B] maximum size of send data buffer (1MB). """
122    SOCK_EVENT_TO = 1.0
123    """ [s] maximum wait time for socket events (blocking timeout for selector.select()). """
124
125    def __init__(self, pluginId:str,
126                 sleepPeriod:float = 0.01,
127                 autoClose:bool = False,
128                 checkPluginId:bool = True,
129                 updateStatesOnBroadcast:bool = False,
130                 maxWorkers:int = None,
131                 executor:Executor = None,
132                 useNamespaceCallbacks:bool = False,
133                 loggerName:str = None,
134                 logLevel:str = "INFO",
135                 logStream:TextIO = sys.stderr,
136                 logFileName:str = None):
137        """
138        Creates an instance of the client.
139
140        Args:
141            `pluginId`: ID string of the TouchPortal plugin using this client. **Required.**
142            `sleepPeriod`: Seconds to sleep the event loop between socket read events. Default: 0.01
143            `autoClose`: If `True` then this client will automatically disconnect when a `closePlugin` message is received from TP.
144                Default is `False`.
145            `checkPluginId`: Validate that `pluginId` matches ours in any messages from TP which contain one (such as actions).
146                Default is `True`.
147            `updateStatesOnBroadcast`: Re-send all cached State values whenever user switches TP page.
148                Default is `True`.
149            `maxWorkers`: Maximum worker threads to run concurrently for event handlers.
150                Default of `None` creates a default-constructed `ThreadPoolExecutor`.
151            `executor`: Passed to `pyee.ExecutorEventEmitter`. By default this is a default-constructed
152                [ThreadPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor),
153                optionally using `maxWorkers` concurrent threads.
154            `useNamespaceCallbacks`: use NamespaceCallback as message handler
155                Default is `False` meaning It will send normal json
156                `True` meaning It will automatically convert json to namespace to make easier access value
157                eg json: data['actionId']['value'] and namespace would be data.actionId.value
158            `loggerName`: Optional name for the Logger to be used by the Client.
159                Default of `None` creates (or uses, if it already exists) the "root" (default) logger.
160            `logLevel`: Desired minimum logging level, one of:
161                "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants),
162                or `None` to disable all logging. The level can also be set at runtime via `setLogLevel()` method.
163                Default is "INFO".
164            `logStream`: Set a stream to write log messages to, or `None` to disable stream (console) logging.
165                The stream logger can also be modified at runtime via `setLogStream()` method.
166                Default is `sys.stderr`.
167            `logFileName`: A file name (with optional path) for log messages. Paths are relative to current working directory.
168                Pass `None` or empty string to disable file logging. The log file is rotated once per day and the last 7
169                logs are preserved (older ones are deleted). The file logger can also be modified at runtime via `setLogFile()` method.
170                Default is `None` (file logging is disabled).
171        """
172        if not executor and maxWorkers:
173            executor = ThreadPoolExecutor(max_workers=maxWorkers)
174        super(Client, self).__init__(executor=executor)
175        self.pluginId = pluginId
176        self.sleepPeriod = sleepPeriod
177        self.autoClose = autoClose
178        self.checkPluginId = checkPluginId
179        self.updateStatesOnBroadcast = updateStatesOnBroadcast
180        self.useNamespaceCallbacks = useNamespaceCallbacks
181        self.log = Logger(name=loggerName, level=logLevel, filename=logFileName, stream=logStream)
182        self.client = None
183        self.selector = None
184        self.currentStates = {}
185        self.currentSettings = {}
186        self.choiceUpdateList = {}
187        self.shortIdTracker = {}
188        self.__heldActions = {}
189        self.__stopEvent = Event()       # main loop inerrupt
190        self.__stopEvent.set()           # not running yet
191        self.__dataReadyEvent = Event()  # set when __sendBuffer has data
192        self.__writeLock = Lock()        # mutex for __sendBuffer
193        self.__sendBuffer = bytearray()
194        self.__recvBuffer = bytearray()
195        # explicitly disable logging if logLevel `None` was passed (Logger() c'tor ignores `None` log level)
196        if not logLevel:
197            self.log.setLogLevel(None)
198
199    def __buffered_readLine(self):
200        try:
201            # Should be ready to read
202            data = self.client.recv(self.RCV_BUFFER_SZ)
203        except BlockingIOError:
204            pass  # Resource temporarily unavailable (errno EWOULDBLOCK)
205        except OSError:
206            raise  # No connection
207        else:
208            if data:
209                lines = []
210                self.__recvBuffer += data
211                while (i := self.__recvBuffer.find(b'\n')) > -1:
212                    lines.append(self.__recvBuffer[:i])
213                    del self.__recvBuffer[:i+1]
214                return lines
215            else:
216                # No connection
217                self.__raiseException("Peer closed the connection.", RuntimeError)
218        return []
219
220    def __write(self):
221        if self.client and self.__sendBuffer and self.__getWriteLock():
222            try:
223                # Should be ready to write
224                sent = self.client.send(self.__sendBuffer)
225            except BlockingIOError:
226                pass  # Resource temporarily unavailable (errno EWOULDBLOCK)
227            except OSError:
228                raise  # No connection
229            else:
230                del self.__sendBuffer[:sent]
231            finally:
232                if not self.__sendBuffer:
233                    self.__dataReadyEvent.clear()
234                self.__writeLock.release()
235
236    def __run(self):
237        try:
238            while not self.__stopEvent.is_set():
239                events = self.selector.select(timeout=self.SOCK_EVENT_TO)
240                if self.__stopEvent.is_set():  # may be set while waiting for selector events (unlikely)
241                    break
242                for _, mask in events:
243                    if (mask & selectors.EVENT_READ):
244                        for line in self.__buffered_readLine():
245                            self.__processMessage(line)
246                    if (mask & selectors.EVENT_WRITE):
247                        self.__write()
248                # Sleep for period or until there is data in the write buffer.
249                # In theory if data is constantly avaiable, this could block,
250                # in which case it may be better to self.__stopEvent.wait()
251                if self.__dataReadyEvent.wait(self.sleepPeriod):
252                    continue
253                continue
254        except Exception as e:
255            self.__die(f"Exception in client event loop: {repr(e)}", e)
256
257    def __processMessage(self, message: bytes):
258        data = json.loads(message.decode())
259        if data and (act_type := data.get('type')):
260            if self.checkPluginId and (pid := data.get('pluginId')) and pid != self.pluginId:
261                return
262            if act_type == TYPES.onShutdown:
263                if self.autoClose: self.__close()
264            elif act_type == TYPES.onHold_down and (aid := data.get('actionId')):
265                self.__heldActions[aid] = True
266            elif act_type == TYPES.onHold_up and (aid := data.get('actionId')):
267                del self.__heldActions[aid]
268            elif act_type == TYPES.onBroadcast and self.updateStatesOnBroadcast:
269                for key, value in self.currentStates.items():
270                    self.__stateUpdate(key, value, True)
271            elif act_type == TYPES.shortConnectorIdNotification:
272                self.shortIdTracker[data["connectorId"]] = data['shortId']
273            self.__emitEvent(act_type, data)
274
275    def __emitEvent(self, ev, data):
276        if not self.useNamespaceCallbacks:
277            self.emit(ev, data)
278            self.emit(TYPES.allMessage, data)
279        else:
280            convertedData = Tools.nested_conversion(data) # No need to call this twice
281            self.emit(ev, convertedData)
282            self.emit(TYPES.allMessage, convertedData)
283
284    def __open(self):
285        try:
286            self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
287            self.selector = selectors.DefaultSelector()
288            self.client.connect((self.TPHOST, self.TPPORT))
289        except Exception:
290            self.selector = self.client = None
291            raise
292        self.client.setblocking(False)
293        self.selector.register(self.client, (selectors.EVENT_READ | selectors.EVENT_WRITE))
294        self.__stopEvent.clear()
295
296    def __close(self):
297        self.log.info(f"{self.pluginId} Disconnected from TouchPortal")
298        self.__stopEvent.set()
299        if self.__writeLock.locked():
300            self.__writeLock.release()
301        self.__sendBuffer.clear()
302        if not self.selector:
303            return
304        if self.selector.get_map():
305            try:
306                self.selector.unregister(self.client)
307            except Exception as e:
308                self.log.warning(f"Error in selector.unregister(): {repr(e)}")
309        try:
310            self.client.shutdown(socket.SHUT_RDWR)
311            self.client.close()
312        except OSError as e:
313            self.log.warning(f"Error in socket.close(): {repr(e)}")
314        finally:
315            # Delete reference to socket object for garbage collection, socket cannot be reused anyway.
316            self.client = None
317        self.selector.close()
318        self.selector = None
319        # print("TP Client stopped.")
320
321    def __die(self, msg=None, exc=None):
322        if msg: self.log.info(msg)
323        self.__emitEvent(TYPES.onShutdown, {"type": TYPES.onShutdown})
324        self.__close()
325        if exc:
326            self.log.critical(repr(exc))
327            raise exc
328
329    def __getWriteLock(self):
330        if self.__writeLock.acquire(timeout=15):
331            if self.__stopEvent.is_set():
332                if self.__writeLock.locked(): self.__writeLock.release()
333                return False
334            return True
335        self.__die(exc=RuntimeError("Send buffer mutex deadlock, cannot continue."))
336        return False
337
338    def __raiseException(self, message, exc = TypeError):
339        self.log.error(message)
340        raise exc(message)
341
342    def isConnected(self):
343        """
344        Returns `True` if the Client is connected to Touch Portal, `False` otherwise.
345        """
346        return not self.__stopEvent.is_set()
347
348    def setLogLevel(self, level):
349        """ Sets the minimum logging level. `level` can be one of one of:
350            "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants),
351            or `None` to disable all logging.
352        """
353        self.log.setLogLevel(level)
354
355    def setLogStream(self, stream):
356        """ Set a destination for the StreamHandler logger. `stream` should be a file stream type (eg. os.stderr) or `None` to disable. """
357        self.log.setStreamDestination(stream)
358
359    def setLogFile(self, fileName):
360        """ Set a destination for the File logger. `filename` should be a file name (with or w/out a path) or `None` to disable the file logger. """
361        self.log.setFileDestination(fileName)
362
363    def isActionBeingHeld(self, actionId:str):
364        """
365        For an Action with ID of `actionId` that can be held (`hasHoldFunctionality` is true),
366        this method returns `True` while it is being held and `False` otherwise.
367        """
368        return actionId in self.__heldActions
369
370    def createState(self, stateId:str, description:str, value:str, parentGroup:str = None):
371        """
372        This will create a TP State at runtime. `stateId`, `description`, and `value` (`value` becomes the State's default value) are all required except `parentGroup` which allow you to create states in a `folder`.
373        """
374        if stateId and description and value != None:
375            if stateId not in self.currentStates:
376                self.send({"type": "createState", "id": stateId, "desc": description, "defaultValue": value, "parentGroup": parentGroup})
377                self.currentStates[stateId] = value
378            else:
379                self.stateUpdate(stateId, value)
380
381    def createStateMany(self, states:list):
382        """
383        Convenience function to create several States at once. `states` should be an iteratable of `dict` types
384        in the form of `{'id': "StateId", 'desc': "Description", 'value': "Default Value"}` and optionally `'parentGroup': "Value"` which will make state `folder`.
385        """
386        try:
387            for state in states:
388                if isinstance(state, dict):
389                    self.createState(state.get('id', ""), state.get('desc', ""), state.get('value', ""), state.get("parentGroup", ""))
390                else:
391                    self.__raiseException(f'createStateMany() requires a list of dicts, got {type(state)} instead.')
392        except:
393            self.__raiseException(f"createStateMany() requires an iteratable, got {type(states)} instead.")
394
395    def removeState(self, stateId:str, validateExists = True):
396        """
397        This removes a State that has been created at runtime. `stateId` is the ID to remove.
398        If `validateExists` is True (default) this method will raise an Exception if the `stateId`
399        does not exist in the current state cache.
400        """
401        if stateId and stateId in self.currentStates:
402            self.send({"type": "removeState", "id": stateId})
403            self.currentStates.pop(stateId)
404        elif validateExists:
405            self.__raiseException(f"{stateId} Does not exist.", Exception)
406
407    def removeStateMany(self, states:list):
408        """
409        Convenience function to remove several States at once. `states` should be an iteratable of state ID strings.
410        This method does not validate if the states exist in the current state cache. See also: `removeState`.
411        """
412        try:
413            for state in states:
414                self.removeState(state, False)
415        except TypeError:
416            self.__raiseException(f'removeStateMany() requires an iteratable, got {type(states)} instead.')
417
418    def choiceUpdate(self, choiceId:str, values:list):
419        """
420        This updates the list of choices in a previously-declared TP State with id `stateId`.
421        See TP API reference for details on updating list values.
422        """
423        if choiceId:
424            if isinstance(values, list):
425                self.send({"type": "choiceUpdate", "id": choiceId, "value": values})
426                self.choiceUpdateList[choiceId] = values
427            else:
428                self.__raiseException(f'choiceUpdate() values argument needs to be a list not a {type(values)}')
429
430    def choiceUpdateSpecific(self, stateId:str, values:list, instanceId:str):
431        """
432        This updates a list of choices in a specific TP Item Instance, specified in `instanceId`.
433        See TP API reference for details on updating specific instances.
434        """
435        if stateId and instanceId:
436            if isinstance(values, list):
437                self.send({"type": "choiceUpdate", "id": stateId, "instanceId": instanceId, "value": values})
438            else:
439                self.__raiseException(f'choiceUpdateSpecific() values argument needs to be a list not a {type(values)}')
440
441    def settingUpdate(self, settingName:str, settingValue):
442        """
443        This updates a value named `settingName` in your plugin's Settings to `settingValue`.
444        """
445        if settingName and settingName not in self.currentSettings or self.currentSettings[settingName] != settingValue:
446            self.send({"type": "settingUpdate", "name": settingName, "value": settingValue})
447            self.currentSettings[settingName] = settingValue
448
449    def stateUpdate(self, stateId:str, stateValue:str):
450        """
451        This updates a value in ether a pre-defined static State or a dynamic State created in runtime.
452        """
453        self.__stateUpdate(stateId, stateValue, False)
454
455    def __stateUpdate(self, stateId:str, stateValue:str, forced:bool):
456        if stateId:
457            if forced or stateId not in self.currentStates or self.currentStates[stateId] != stateValue:
458                self.send({"type": "stateUpdate", "id": stateId, "value": stateValue})
459            self.currentStates[stateId] = stateValue
460
461    def stateUpdateMany(self, states:list):
462        """
463        Convenience function to update several states at once.
464        `states` should be an iteratable of `dict` types in the form of `{'id': "StateId", 'value': "The New Value"}`.
465        """
466        try:
467            for state in states:
468                if isinstance(state, dict):
469                    self.stateUpdate(state.get('id', ""), state.get('value', ""))
470                else:
471                    self.__raiseException(f'StateUpdateMany() requires a list of dicts, got {type(state)} instead.')
472        except TypeError:
473            self.__raiseException(f"createStateMany() requires an iteratable, got {type(states)} instead.")
474
475    def showNotification(self, notificationId:str, title:str, msg:str, options:list):
476        """
477        This method allows your plugin to send a notification to Touch Portal with custom title, message body and available user action(s).
478        Requires TP SDK v4.0 or higher.
479
480        Args:
481            `notificationId`: Unique ID of this notification.
482            `title`: The notification title.
483            `msg`: The message body text that is shown in the notifcation.
484            `options`: List of options (actions) for the notification. Each option should be a `dict` type with `id` and `title` keys.
485        """
486        if notificationId and title and msg and options and isinstance(options, list):
487            for option in options:
488                if 'id' not in option.keys() or 'title' not in option.keys():
489                    self.__raiseException("all options require id and title keys")
490            self.send({
491                "type": "showNotification",
492                "notificationId": str(notificationId),
493                "title": str(title),
494                "msg": str(msg),
495                "options": options
496            })
497
498    def __findShortId(self, connectorId:str):
499        """        
500        This method is used internally to find the short ID of a connector.
501
502        UNUSED code.
503        """
504        for cid in list(self.shortIdTracker.keys()):
505            if (splitCId := connectorId.split("|")) and splitCId[0] == cid.split("|")[0]:
506                if all(x in splitCId for x in splitCId[1:] if x in cid.split("|")[1:]):
507                    return self.shortIdTracker[connectorId]
508        return None
509
510    def shortIdUpdate(self, shortId:str, connectorValue:int):
511        """
512        This allows you to update slider position value using shortId which TouchPortal will broadcast.
513
514        Args:
515            `shortId`: a shortId is a id that is mapped to connectorId by TouchPortal
516            `connectorValue`: A integer between 0-100
517
518        """
519        if 0 <= connectorValue <= 100:
520            self.send({
521                "type": "connectorUpdate",
522                "shortId": shortId,
523                "value": str(connectorValue)
524            })
525
526    def connectorUpdate(self, connectorId:str, connectorValue:int):
527        """
528        This allows you to update slider position value.
529
530        Args:
531            `connectorId`: Cannot be longer then 200 characters.
532                connectorId have syntax that you need to follow https://www.touch-portal.com/api/index.php?section=connectors
533                Also according to that site It requires you to have prefix "pc_yourPluginId_" + connectorid however This already provide
534                you the prefix and the pluginId so you just need you take care the rest eg connectorid|setting1=aValue
535            `connectorValue`: Must be an integer between 0-100.
536
537        Note: This method will automatically looking for shortId using gaven connectorId however if It cannot find shortId it will just send connectorId
538        """
539        if not isinstance(connectorId, str):
540            self.__raiseException(f"connectorId needs to be a str not a {type(connectorId)}")
541        if not isinstance(connectorValue, int):
542            self.__raiseException(f"connectorValue requires a int not {type(connectorValue)}")
543        if 0 <= connectorValue <= 100:
544            if f"pc_{self.pluginId}_{connectorId}" in self.shortIdTracker:
545                self.shortIdUpdate(self.shortIdTracker[connectorId], connectorValue)
546            else:
547                self.send({
548                    "type": "connectorUpdate",
549                    "connectorId": f"pc_{self.pluginId}_{connectorId}",
550                    "value": str(connectorValue)
551                })
552        else:
553            self.__raiseException(f"connectorValue needs to be between 0-100 not {connectorValue}")
554
555    def updateActionData(self, instanceId:str, stateId:str, minValue, maxValue):
556        """
557        This allows you to update Action Data in one of your Action. Currently TouchPortal only supports changing the minimum and maximum values in numeric data types.
558        """
559        self.send({"type": "updateActionData", "instanceId": instanceId, "data": {"minValue": minValue, "maxValue": maxValue, "id": stateId, "type": "number"}})
560
561    def getChoiceUpdatelist(self):
562        """
563        This will return a dict that `choiceUpdate` registered.
564            example return value `{"choiceUpdateid1": ["item1", "item2", "item3"], "exampleChoiceId": ["Option1", "Option2", "Option3"]}`
565
566        You should use this to verify before Updating the choice list
567        **Note** This is the same as TPClient.choiceUpdateList variable *DO NOT MODIFY* TPClient.choiceUpdateList unless you know what your doing
568        """
569        return self.choiceUpdateList
570
571    def getStatelist(self):
572        """
573        This will return a dict that have key pair of states that you last updated.
574            Example retun value `{"stateId1": "value1", "stateId2": "value2", "stateId3": "value3"}`
575        This is used to keep track of all states. It will be automatically updated when you update states
576        **Note** This is the same as TPClient.currentState variable *DO NOT MODIFY* TPClient.currentState unless you know what your doing
577        """
578        return self.currentStates
579
580    def getSettinghistory(self):
581        """
582        This will return a dict that have key pair of setting value that you updated previously.
583
584        This is used to track settings value that you have updated previously
585        **Note** This is the same as TPClient.currentSettings variable *DO NOT MODIFY* TPClient.currentSettings unless you know what your doing
586        """
587        return self.currentSettings
588
589    def send(self, data):
590        """
591        This will try to send any arbitrary Python object in `data` (presumably something `dict`-like) to Touch Portal
592        after serializing it as JSON and adding a `\n`. Normally there is no need to use this method directly, but if the
593        Python API doesn't cover something from the TP API, this could be used instead.
594        """
595        if not self.isConnected():
596            self.__raiseException("TP Client not connected to Touch Portal, cannot send commands.", Exception)
597        if self.__getWriteLock():
598            if len(self.__sendBuffer) + len(data) > self.SND_BUFFER_SZ:
599                self.__writeLock.release()
600                self.__raiseException("TP Client send buffer is full!", ResourceWarning)
601            self.__sendBuffer += (json.dumps(data)+'\n').encode()
602            self.__writeLock.release()
603            self.__dataReadyEvent.set()
604
605    def connect(self):
606        """
607        Initiate connection to TP Server.
608        If successful, it starts the main processing loop of this client.
609        Does nothing if client is already connected.
610
611        **Note** that `connect()` blocks further execution of your script until one of the following occurs:
612          - `disconnect()` is called in an event handler,
613          - TP sends `closePlugin` message and `autoClose` is `True`
614          - or an internal error occurs (for example Touch Portal disconnects unexpectedly)
615        """
616        if not self.isConnected():
617            self.__open()
618            self.send({"type":"pair", "id": self.pluginId})
619            self.__run()  # start the event loop
620
621    def disconnect(self):
622        """
623        This closes the connection to TP and terminates the client processing loop.
624        Does nothing if client is already disconnected.
625        """
626        if self.isConnected():
627            self.__close()
628
629    @staticmethod
630    def getActionDataValue(data:list, valueId:str=None):
631        """
632        Utility for processing action messages from TP. For example:
633            {"type": "action", "data": [{ "id": "data object id", "value": "user specified value" }, ...]}
634
635        Returns the `value` with specific `id` from a list of action data,
636        or `None` if the `id` wasn't found. If a null id is passed in `valueId`
637        then the first entry which has a `value` key, if any, will be returned.
638
639        Args:
640            `data`: the "data" array from a TP "action", "on", or "off" message
641            `valueId`: the "id" to look for in `data`. `None` or blank to return the first value found.
642        """
643        if not data: return None
644        if valueId:
645            return next((x.get('value') for x in data if x.get('id', '') == valueId), None)
646        return next((x.get('value') for x in data if x.get('value') != None), None)
class Client(pyee.ExecutorEventEmitter):
 88class Client(ExecutorEventEmitter):
 89    """
 90    A TCP/IP client for [Touch Portal API](https://www.touch-portal.com/api) plugin integration.
 91    Implements a [pyee.ExecutorEventEmitter](https://pyee.readthedocs.io/en/latest/#pyee.ExecutorEventEmitter).
 92
 93    After an initial connection to a Touch Portal desktop application "server," the client
 94    implements a send/receive event loop while maintaining the open sockets. Messages between
 95    TP and the plugin are exchanged asynchronously, with all sending methods possibly returning
 96    before the actual data is sent.
 97
 98    Messages from Touch Portal are delivered to the plugin script via event handler callbacks,
 99    which can be either individual callbacks per message type, and/or a single handler for all
100    types of messages. The callbacks are executed in separate Thread(s), using a pool of up to
101    any number of concurrent threads as needed (if using more than one thread, the plugin
102    code itself is responsible for thread safety of its internal data). Please check the
103    `pyee.EventEmitter` documentation for general information on how events are handled
104    (which can be either via function decorators or via the inherited `Client.on()` method).
105
106    Generated event names correspond to Touch Portal API message types, one for each type
107    as defined in the TP API documentation (eg. "action" or "listChange") as well as one event
108    which is generated for all message types ("any"). Alternately, for a more formal approach,
109    the corresponding members of the `TYPES` class could be used instead of string names
110    (eg. `TYPES.onAction` or `TYPES.onListChange`).
111
112    Any errors raised within event listeners/handlers are trapped and then reported in the
113    inherited "error" event (from pyee.ExecutorEventEmitter), aka `TYPES.onError`.
114    """
115    TPHOST = '127.0.0.1'
116    """ TP plugin server host IPv4 address. """
117    TPPORT = 12136
118    """ TP plugin server host IPv4 port number. """
119    RCV_BUFFER_SZ = 4096
120    """ [B] incoming data buffer size. """
121    SND_BUFFER_SZ = 32**4
122    """ [B] maximum size of send data buffer (1MB). """
123    SOCK_EVENT_TO = 1.0
124    """ [s] maximum wait time for socket events (blocking timeout for selector.select()). """
125
126    def __init__(self, pluginId:str,
127                 sleepPeriod:float = 0.01,
128                 autoClose:bool = False,
129                 checkPluginId:bool = True,
130                 updateStatesOnBroadcast:bool = False,
131                 maxWorkers:int = None,
132                 executor:Executor = None,
133                 useNamespaceCallbacks:bool = False,
134                 loggerName:str = None,
135                 logLevel:str = "INFO",
136                 logStream:TextIO = sys.stderr,
137                 logFileName:str = None):
138        """
139        Creates an instance of the client.
140
141        Args:
142            `pluginId`: ID string of the TouchPortal plugin using this client. **Required.**
143            `sleepPeriod`: Seconds to sleep the event loop between socket read events. Default: 0.01
144            `autoClose`: If `True` then this client will automatically disconnect when a `closePlugin` message is received from TP.
145                Default is `False`.
146            `checkPluginId`: Validate that `pluginId` matches ours in any messages from TP which contain one (such as actions).
147                Default is `True`.
148            `updateStatesOnBroadcast`: Re-send all cached State values whenever user switches TP page.
149                Default is `True`.
150            `maxWorkers`: Maximum worker threads to run concurrently for event handlers.
151                Default of `None` creates a default-constructed `ThreadPoolExecutor`.
152            `executor`: Passed to `pyee.ExecutorEventEmitter`. By default this is a default-constructed
153                [ThreadPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor),
154                optionally using `maxWorkers` concurrent threads.
155            `useNamespaceCallbacks`: use NamespaceCallback as message handler
156                Default is `False` meaning It will send normal json
157                `True` meaning It will automatically convert json to namespace to make easier access value
158                eg json: data['actionId']['value'] and namespace would be data.actionId.value
159            `loggerName`: Optional name for the Logger to be used by the Client.
160                Default of `None` creates (or uses, if it already exists) the "root" (default) logger.
161            `logLevel`: Desired minimum logging level, one of:
162                "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants),
163                or `None` to disable all logging. The level can also be set at runtime via `setLogLevel()` method.
164                Default is "INFO".
165            `logStream`: Set a stream to write log messages to, or `None` to disable stream (console) logging.
166                The stream logger can also be modified at runtime via `setLogStream()` method.
167                Default is `sys.stderr`.
168            `logFileName`: A file name (with optional path) for log messages. Paths are relative to current working directory.
169                Pass `None` or empty string to disable file logging. The log file is rotated once per day and the last 7
170                logs are preserved (older ones are deleted). The file logger can also be modified at runtime via `setLogFile()` method.
171                Default is `None` (file logging is disabled).
172        """
173        if not executor and maxWorkers:
174            executor = ThreadPoolExecutor(max_workers=maxWorkers)
175        super(Client, self).__init__(executor=executor)
176        self.pluginId = pluginId
177        self.sleepPeriod = sleepPeriod
178        self.autoClose = autoClose
179        self.checkPluginId = checkPluginId
180        self.updateStatesOnBroadcast = updateStatesOnBroadcast
181        self.useNamespaceCallbacks = useNamespaceCallbacks
182        self.log = Logger(name=loggerName, level=logLevel, filename=logFileName, stream=logStream)
183        self.client = None
184        self.selector = None
185        self.currentStates = {}
186        self.currentSettings = {}
187        self.choiceUpdateList = {}
188        self.shortIdTracker = {}
189        self.__heldActions = {}
190        self.__stopEvent = Event()       # main loop inerrupt
191        self.__stopEvent.set()           # not running yet
192        self.__dataReadyEvent = Event()  # set when __sendBuffer has data
193        self.__writeLock = Lock()        # mutex for __sendBuffer
194        self.__sendBuffer = bytearray()
195        self.__recvBuffer = bytearray()
196        # explicitly disable logging if logLevel `None` was passed (Logger() c'tor ignores `None` log level)
197        if not logLevel:
198            self.log.setLogLevel(None)
199
200    def __buffered_readLine(self):
201        try:
202            # Should be ready to read
203            data = self.client.recv(self.RCV_BUFFER_SZ)
204        except BlockingIOError:
205            pass  # Resource temporarily unavailable (errno EWOULDBLOCK)
206        except OSError:
207            raise  # No connection
208        else:
209            if data:
210                lines = []
211                self.__recvBuffer += data
212                while (i := self.__recvBuffer.find(b'\n')) > -1:
213                    lines.append(self.__recvBuffer[:i])
214                    del self.__recvBuffer[:i+1]
215                return lines
216            else:
217                # No connection
218                self.__raiseException("Peer closed the connection.", RuntimeError)
219        return []
220
221    def __write(self):
222        if self.client and self.__sendBuffer and self.__getWriteLock():
223            try:
224                # Should be ready to write
225                sent = self.client.send(self.__sendBuffer)
226            except BlockingIOError:
227                pass  # Resource temporarily unavailable (errno EWOULDBLOCK)
228            except OSError:
229                raise  # No connection
230            else:
231                del self.__sendBuffer[:sent]
232            finally:
233                if not self.__sendBuffer:
234                    self.__dataReadyEvent.clear()
235                self.__writeLock.release()
236
237    def __run(self):
238        try:
239            while not self.__stopEvent.is_set():
240                events = self.selector.select(timeout=self.SOCK_EVENT_TO)
241                if self.__stopEvent.is_set():  # may be set while waiting for selector events (unlikely)
242                    break
243                for _, mask in events:
244                    if (mask & selectors.EVENT_READ):
245                        for line in self.__buffered_readLine():
246                            self.__processMessage(line)
247                    if (mask & selectors.EVENT_WRITE):
248                        self.__write()
249                # Sleep for period or until there is data in the write buffer.
250                # In theory if data is constantly avaiable, this could block,
251                # in which case it may be better to self.__stopEvent.wait()
252                if self.__dataReadyEvent.wait(self.sleepPeriod):
253                    continue
254                continue
255        except Exception as e:
256            self.__die(f"Exception in client event loop: {repr(e)}", e)
257
258    def __processMessage(self, message: bytes):
259        data = json.loads(message.decode())
260        if data and (act_type := data.get('type')):
261            if self.checkPluginId and (pid := data.get('pluginId')) and pid != self.pluginId:
262                return
263            if act_type == TYPES.onShutdown:
264                if self.autoClose: self.__close()
265            elif act_type == TYPES.onHold_down and (aid := data.get('actionId')):
266                self.__heldActions[aid] = True
267            elif act_type == TYPES.onHold_up and (aid := data.get('actionId')):
268                del self.__heldActions[aid]
269            elif act_type == TYPES.onBroadcast and self.updateStatesOnBroadcast:
270                for key, value in self.currentStates.items():
271                    self.__stateUpdate(key, value, True)
272            elif act_type == TYPES.shortConnectorIdNotification:
273                self.shortIdTracker[data["connectorId"]] = data['shortId']
274            self.__emitEvent(act_type, data)
275
276    def __emitEvent(self, ev, data):
277        if not self.useNamespaceCallbacks:
278            self.emit(ev, data)
279            self.emit(TYPES.allMessage, data)
280        else:
281            convertedData = Tools.nested_conversion(data) # No need to call this twice
282            self.emit(ev, convertedData)
283            self.emit(TYPES.allMessage, convertedData)
284
285    def __open(self):
286        try:
287            self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
288            self.selector = selectors.DefaultSelector()
289            self.client.connect((self.TPHOST, self.TPPORT))
290        except Exception:
291            self.selector = self.client = None
292            raise
293        self.client.setblocking(False)
294        self.selector.register(self.client, (selectors.EVENT_READ | selectors.EVENT_WRITE))
295        self.__stopEvent.clear()
296
297    def __close(self):
298        self.log.info(f"{self.pluginId} Disconnected from TouchPortal")
299        self.__stopEvent.set()
300        if self.__writeLock.locked():
301            self.__writeLock.release()
302        self.__sendBuffer.clear()
303        if not self.selector:
304            return
305        if self.selector.get_map():
306            try:
307                self.selector.unregister(self.client)
308            except Exception as e:
309                self.log.warning(f"Error in selector.unregister(): {repr(e)}")
310        try:
311            self.client.shutdown(socket.SHUT_RDWR)
312            self.client.close()
313        except OSError as e:
314            self.log.warning(f"Error in socket.close(): {repr(e)}")
315        finally:
316            # Delete reference to socket object for garbage collection, socket cannot be reused anyway.
317            self.client = None
318        self.selector.close()
319        self.selector = None
320        # print("TP Client stopped.")
321
322    def __die(self, msg=None, exc=None):
323        if msg: self.log.info(msg)
324        self.__emitEvent(TYPES.onShutdown, {"type": TYPES.onShutdown})
325        self.__close()
326        if exc:
327            self.log.critical(repr(exc))
328            raise exc
329
330    def __getWriteLock(self):
331        if self.__writeLock.acquire(timeout=15):
332            if self.__stopEvent.is_set():
333                if self.__writeLock.locked(): self.__writeLock.release()
334                return False
335            return True
336        self.__die(exc=RuntimeError("Send buffer mutex deadlock, cannot continue."))
337        return False
338
339    def __raiseException(self, message, exc = TypeError):
340        self.log.error(message)
341        raise exc(message)
342
343    def isConnected(self):
344        """
345        Returns `True` if the Client is connected to Touch Portal, `False` otherwise.
346        """
347        return not self.__stopEvent.is_set()
348
349    def setLogLevel(self, level):
350        """ Sets the minimum logging level. `level` can be one of one of:
351            "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants),
352            or `None` to disable all logging.
353        """
354        self.log.setLogLevel(level)
355
356    def setLogStream(self, stream):
357        """ Set a destination for the StreamHandler logger. `stream` should be a file stream type (eg. os.stderr) or `None` to disable. """
358        self.log.setStreamDestination(stream)
359
360    def setLogFile(self, fileName):
361        """ Set a destination for the File logger. `filename` should be a file name (with or w/out a path) or `None` to disable the file logger. """
362        self.log.setFileDestination(fileName)
363
364    def isActionBeingHeld(self, actionId:str):
365        """
366        For an Action with ID of `actionId` that can be held (`hasHoldFunctionality` is true),
367        this method returns `True` while it is being held and `False` otherwise.
368        """
369        return actionId in self.__heldActions
370
371    def createState(self, stateId:str, description:str, value:str, parentGroup:str = None):
372        """
373        This will create a TP State at runtime. `stateId`, `description`, and `value` (`value` becomes the State's default value) are all required except `parentGroup` which allow you to create states in a `folder`.
374        """
375        if stateId and description and value != None:
376            if stateId not in self.currentStates:
377                self.send({"type": "createState", "id": stateId, "desc": description, "defaultValue": value, "parentGroup": parentGroup})
378                self.currentStates[stateId] = value
379            else:
380                self.stateUpdate(stateId, value)
381
382    def createStateMany(self, states:list):
383        """
384        Convenience function to create several States at once. `states` should be an iteratable of `dict` types
385        in the form of `{'id': "StateId", 'desc': "Description", 'value': "Default Value"}` and optionally `'parentGroup': "Value"` which will make state `folder`.
386        """
387        try:
388            for state in states:
389                if isinstance(state, dict):
390                    self.createState(state.get('id', ""), state.get('desc', ""), state.get('value', ""), state.get("parentGroup", ""))
391                else:
392                    self.__raiseException(f'createStateMany() requires a list of dicts, got {type(state)} instead.')
393        except:
394            self.__raiseException(f"createStateMany() requires an iteratable, got {type(states)} instead.")
395
396    def removeState(self, stateId:str, validateExists = True):
397        """
398        This removes a State that has been created at runtime. `stateId` is the ID to remove.
399        If `validateExists` is True (default) this method will raise an Exception if the `stateId`
400        does not exist in the current state cache.
401        """
402        if stateId and stateId in self.currentStates:
403            self.send({"type": "removeState", "id": stateId})
404            self.currentStates.pop(stateId)
405        elif validateExists:
406            self.__raiseException(f"{stateId} Does not exist.", Exception)
407
408    def removeStateMany(self, states:list):
409        """
410        Convenience function to remove several States at once. `states` should be an iteratable of state ID strings.
411        This method does not validate if the states exist in the current state cache. See also: `removeState`.
412        """
413        try:
414            for state in states:
415                self.removeState(state, False)
416        except TypeError:
417            self.__raiseException(f'removeStateMany() requires an iteratable, got {type(states)} instead.')
418
419    def choiceUpdate(self, choiceId:str, values:list):
420        """
421        This updates the list of choices in a previously-declared TP State with id `stateId`.
422        See TP API reference for details on updating list values.
423        """
424        if choiceId:
425            if isinstance(values, list):
426                self.send({"type": "choiceUpdate", "id": choiceId, "value": values})
427                self.choiceUpdateList[choiceId] = values
428            else:
429                self.__raiseException(f'choiceUpdate() values argument needs to be a list not a {type(values)}')
430
431    def choiceUpdateSpecific(self, stateId:str, values:list, instanceId:str):
432        """
433        This updates a list of choices in a specific TP Item Instance, specified in `instanceId`.
434        See TP API reference for details on updating specific instances.
435        """
436        if stateId and instanceId:
437            if isinstance(values, list):
438                self.send({"type": "choiceUpdate", "id": stateId, "instanceId": instanceId, "value": values})
439            else:
440                self.__raiseException(f'choiceUpdateSpecific() values argument needs to be a list not a {type(values)}')
441
442    def settingUpdate(self, settingName:str, settingValue):
443        """
444        This updates a value named `settingName` in your plugin's Settings to `settingValue`.
445        """
446        if settingName and settingName not in self.currentSettings or self.currentSettings[settingName] != settingValue:
447            self.send({"type": "settingUpdate", "name": settingName, "value": settingValue})
448            self.currentSettings[settingName] = settingValue
449
450    def stateUpdate(self, stateId:str, stateValue:str):
451        """
452        This updates a value in ether a pre-defined static State or a dynamic State created in runtime.
453        """
454        self.__stateUpdate(stateId, stateValue, False)
455
456    def __stateUpdate(self, stateId:str, stateValue:str, forced:bool):
457        if stateId:
458            if forced or stateId not in self.currentStates or self.currentStates[stateId] != stateValue:
459                self.send({"type": "stateUpdate", "id": stateId, "value": stateValue})
460            self.currentStates[stateId] = stateValue
461
462    def stateUpdateMany(self, states:list):
463        """
464        Convenience function to update several states at once.
465        `states` should be an iteratable of `dict` types in the form of `{'id': "StateId", 'value': "The New Value"}`.
466        """
467        try:
468            for state in states:
469                if isinstance(state, dict):
470                    self.stateUpdate(state.get('id', ""), state.get('value', ""))
471                else:
472                    self.__raiseException(f'StateUpdateMany() requires a list of dicts, got {type(state)} instead.')
473        except TypeError:
474            self.__raiseException(f"createStateMany() requires an iteratable, got {type(states)} instead.")
475
476    def showNotification(self, notificationId:str, title:str, msg:str, options:list):
477        """
478        This method allows your plugin to send a notification to Touch Portal with custom title, message body and available user action(s).
479        Requires TP SDK v4.0 or higher.
480
481        Args:
482            `notificationId`: Unique ID of this notification.
483            `title`: The notification title.
484            `msg`: The message body text that is shown in the notifcation.
485            `options`: List of options (actions) for the notification. Each option should be a `dict` type with `id` and `title` keys.
486        """
487        if notificationId and title and msg and options and isinstance(options, list):
488            for option in options:
489                if 'id' not in option.keys() or 'title' not in option.keys():
490                    self.__raiseException("all options require id and title keys")
491            self.send({
492                "type": "showNotification",
493                "notificationId": str(notificationId),
494                "title": str(title),
495                "msg": str(msg),
496                "options": options
497            })
498
499    def __findShortId(self, connectorId:str):
500        """        
501        This method is used internally to find the short ID of a connector.
502
503        UNUSED code.
504        """
505        for cid in list(self.shortIdTracker.keys()):
506            if (splitCId := connectorId.split("|")) and splitCId[0] == cid.split("|")[0]:
507                if all(x in splitCId for x in splitCId[1:] if x in cid.split("|")[1:]):
508                    return self.shortIdTracker[connectorId]
509        return None
510
511    def shortIdUpdate(self, shortId:str, connectorValue:int):
512        """
513        This allows you to update slider position value using shortId which TouchPortal will broadcast.
514
515        Args:
516            `shortId`: a shortId is a id that is mapped to connectorId by TouchPortal
517            `connectorValue`: A integer between 0-100
518
519        """
520        if 0 <= connectorValue <= 100:
521            self.send({
522                "type": "connectorUpdate",
523                "shortId": shortId,
524                "value": str(connectorValue)
525            })
526
527    def connectorUpdate(self, connectorId:str, connectorValue:int):
528        """
529        This allows you to update slider position value.
530
531        Args:
532            `connectorId`: Cannot be longer then 200 characters.
533                connectorId have syntax that you need to follow https://www.touch-portal.com/api/index.php?section=connectors
534                Also according to that site It requires you to have prefix "pc_yourPluginId_" + connectorid however This already provide
535                you the prefix and the pluginId so you just need you take care the rest eg connectorid|setting1=aValue
536            `connectorValue`: Must be an integer between 0-100.
537
538        Note: This method will automatically looking for shortId using gaven connectorId however if It cannot find shortId it will just send connectorId
539        """
540        if not isinstance(connectorId, str):
541            self.__raiseException(f"connectorId needs to be a str not a {type(connectorId)}")
542        if not isinstance(connectorValue, int):
543            self.__raiseException(f"connectorValue requires a int not {type(connectorValue)}")
544        if 0 <= connectorValue <= 100:
545            if f"pc_{self.pluginId}_{connectorId}" in self.shortIdTracker:
546                self.shortIdUpdate(self.shortIdTracker[connectorId], connectorValue)
547            else:
548                self.send({
549                    "type": "connectorUpdate",
550                    "connectorId": f"pc_{self.pluginId}_{connectorId}",
551                    "value": str(connectorValue)
552                })
553        else:
554            self.__raiseException(f"connectorValue needs to be between 0-100 not {connectorValue}")
555
556    def updateActionData(self, instanceId:str, stateId:str, minValue, maxValue):
557        """
558        This allows you to update Action Data in one of your Action. Currently TouchPortal only supports changing the minimum and maximum values in numeric data types.
559        """
560        self.send({"type": "updateActionData", "instanceId": instanceId, "data": {"minValue": minValue, "maxValue": maxValue, "id": stateId, "type": "number"}})
561
562    def getChoiceUpdatelist(self):
563        """
564        This will return a dict that `choiceUpdate` registered.
565            example return value `{"choiceUpdateid1": ["item1", "item2", "item3"], "exampleChoiceId": ["Option1", "Option2", "Option3"]}`
566
567        You should use this to verify before Updating the choice list
568        **Note** This is the same as TPClient.choiceUpdateList variable *DO NOT MODIFY* TPClient.choiceUpdateList unless you know what your doing
569        """
570        return self.choiceUpdateList
571
572    def getStatelist(self):
573        """
574        This will return a dict that have key pair of states that you last updated.
575            Example retun value `{"stateId1": "value1", "stateId2": "value2", "stateId3": "value3"}`
576        This is used to keep track of all states. It will be automatically updated when you update states
577        **Note** This is the same as TPClient.currentState variable *DO NOT MODIFY* TPClient.currentState unless you know what your doing
578        """
579        return self.currentStates
580
581    def getSettinghistory(self):
582        """
583        This will return a dict that have key pair of setting value that you updated previously.
584
585        This is used to track settings value that you have updated previously
586        **Note** This is the same as TPClient.currentSettings variable *DO NOT MODIFY* TPClient.currentSettings unless you know what your doing
587        """
588        return self.currentSettings
589
590    def send(self, data):
591        """
592        This will try to send any arbitrary Python object in `data` (presumably something `dict`-like) to Touch Portal
593        after serializing it as JSON and adding a `\n`. Normally there is no need to use this method directly, but if the
594        Python API doesn't cover something from the TP API, this could be used instead.
595        """
596        if not self.isConnected():
597            self.__raiseException("TP Client not connected to Touch Portal, cannot send commands.", Exception)
598        if self.__getWriteLock():
599            if len(self.__sendBuffer) + len(data) > self.SND_BUFFER_SZ:
600                self.__writeLock.release()
601                self.__raiseException("TP Client send buffer is full!", ResourceWarning)
602            self.__sendBuffer += (json.dumps(data)+'\n').encode()
603            self.__writeLock.release()
604            self.__dataReadyEvent.set()
605
606    def connect(self):
607        """
608        Initiate connection to TP Server.
609        If successful, it starts the main processing loop of this client.
610        Does nothing if client is already connected.
611
612        **Note** that `connect()` blocks further execution of your script until one of the following occurs:
613          - `disconnect()` is called in an event handler,
614          - TP sends `closePlugin` message and `autoClose` is `True`
615          - or an internal error occurs (for example Touch Portal disconnects unexpectedly)
616        """
617        if not self.isConnected():
618            self.__open()
619            self.send({"type":"pair", "id": self.pluginId})
620            self.__run()  # start the event loop
621
622    def disconnect(self):
623        """
624        This closes the connection to TP and terminates the client processing loop.
625        Does nothing if client is already disconnected.
626        """
627        if self.isConnected():
628            self.__close()
629
630    @staticmethod
631    def getActionDataValue(data:list, valueId:str=None):
632        """
633        Utility for processing action messages from TP. For example:
634            {"type": "action", "data": [{ "id": "data object id", "value": "user specified value" }, ...]}
635
636        Returns the `value` with specific `id` from a list of action data,
637        or `None` if the `id` wasn't found. If a null id is passed in `valueId`
638        then the first entry which has a `value` key, if any, will be returned.
639
640        Args:
641            `data`: the "data" array from a TP "action", "on", or "off" message
642            `valueId`: the "id" to look for in `data`. `None` or blank to return the first value found.
643        """
644        if not data: return None
645        if valueId:
646            return next((x.get('value') for x in data if x.get('id', '') == valueId), None)
647        return next((x.get('value') for x in data if x.get('value') != None), None)

A TCP/IP client for Touch Portal API plugin integration. Implements a pyee.ExecutorEventEmitter.

After an initial connection to a Touch Portal desktop application "server," the client implements a send/receive event loop while maintaining the open sockets. Messages between TP and the plugin are exchanged asynchronously, with all sending methods possibly returning before the actual data is sent.

Messages from Touch Portal are delivered to the plugin script via event handler callbacks, which can be either individual callbacks per message type, and/or a single handler for all types of messages. The callbacks are executed in separate Thread(s), using a pool of up to any number of concurrent threads as needed (if using more than one thread, the plugin code itself is responsible for thread safety of its internal data). Please check the pyee.EventEmitter documentation for general information on how events are handled (which can be either via function decorators or via the inherited Client.on() method).

Generated event names correspond to Touch Portal API message types, one for each type as defined in the TP API documentation (eg. "action" or "listChange") as well as one event which is generated for all message types ("any"). Alternately, for a more formal approach, the corresponding members of the TYPES class could be used instead of string names (eg. TYPES.onAction or TYPES.onListChange).

Any errors raised within event listeners/handlers are trapped and then reported in the inherited "error" event (from pyee.ExecutorEventEmitter), aka TYPES.onError.

Client( pluginId: str, sleepPeriod: float = 0.01, autoClose: bool = False, checkPluginId: bool = True, updateStatesOnBroadcast: bool = False, maxWorkers: int = None, executor: concurrent.futures._base.Executor = None, useNamespaceCallbacks: bool = False, loggerName: str = None, logLevel: str = 'INFO', logStream: = <_io.TextIOWrapper name='' mode='w' encoding='utf-8'>, logFileName: str = None)
126    def __init__(self, pluginId:str,
127                 sleepPeriod:float = 0.01,
128                 autoClose:bool = False,
129                 checkPluginId:bool = True,
130                 updateStatesOnBroadcast:bool = False,
131                 maxWorkers:int = None,
132                 executor:Executor = None,
133                 useNamespaceCallbacks:bool = False,
134                 loggerName:str = None,
135                 logLevel:str = "INFO",
136                 logStream:TextIO = sys.stderr,
137                 logFileName:str = None):
138        """
139        Creates an instance of the client.
140
141        Args:
142            `pluginId`: ID string of the TouchPortal plugin using this client. **Required.**
143            `sleepPeriod`: Seconds to sleep the event loop between socket read events. Default: 0.01
144            `autoClose`: If `True` then this client will automatically disconnect when a `closePlugin` message is received from TP.
145                Default is `False`.
146            `checkPluginId`: Validate that `pluginId` matches ours in any messages from TP which contain one (such as actions).
147                Default is `True`.
148            `updateStatesOnBroadcast`: Re-send all cached State values whenever user switches TP page.
149                Default is `True`.
150            `maxWorkers`: Maximum worker threads to run concurrently for event handlers.
151                Default of `None` creates a default-constructed `ThreadPoolExecutor`.
152            `executor`: Passed to `pyee.ExecutorEventEmitter`. By default this is a default-constructed
153                [ThreadPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor),
154                optionally using `maxWorkers` concurrent threads.
155            `useNamespaceCallbacks`: use NamespaceCallback as message handler
156                Default is `False` meaning It will send normal json
157                `True` meaning It will automatically convert json to namespace to make easier access value
158                eg json: data['actionId']['value'] and namespace would be data.actionId.value
159            `loggerName`: Optional name for the Logger to be used by the Client.
160                Default of `None` creates (or uses, if it already exists) the "root" (default) logger.
161            `logLevel`: Desired minimum logging level, one of:
162                "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants),
163                or `None` to disable all logging. The level can also be set at runtime via `setLogLevel()` method.
164                Default is "INFO".
165            `logStream`: Set a stream to write log messages to, or `None` to disable stream (console) logging.
166                The stream logger can also be modified at runtime via `setLogStream()` method.
167                Default is `sys.stderr`.
168            `logFileName`: A file name (with optional path) for log messages. Paths are relative to current working directory.
169                Pass `None` or empty string to disable file logging. The log file is rotated once per day and the last 7
170                logs are preserved (older ones are deleted). The file logger can also be modified at runtime via `setLogFile()` method.
171                Default is `None` (file logging is disabled).
172        """
173        if not executor and maxWorkers:
174            executor = ThreadPoolExecutor(max_workers=maxWorkers)
175        super(Client, self).__init__(executor=executor)
176        self.pluginId = pluginId
177        self.sleepPeriod = sleepPeriod
178        self.autoClose = autoClose
179        self.checkPluginId = checkPluginId
180        self.updateStatesOnBroadcast = updateStatesOnBroadcast
181        self.useNamespaceCallbacks = useNamespaceCallbacks
182        self.log = Logger(name=loggerName, level=logLevel, filename=logFileName, stream=logStream)
183        self.client = None
184        self.selector = None
185        self.currentStates = {}
186        self.currentSettings = {}
187        self.choiceUpdateList = {}
188        self.shortIdTracker = {}
189        self.__heldActions = {}
190        self.__stopEvent = Event()       # main loop inerrupt
191        self.__stopEvent.set()           # not running yet
192        self.__dataReadyEvent = Event()  # set when __sendBuffer has data
193        self.__writeLock = Lock()        # mutex for __sendBuffer
194        self.__sendBuffer = bytearray()
195        self.__recvBuffer = bytearray()
196        # explicitly disable logging if logLevel `None` was passed (Logger() c'tor ignores `None` log level)
197        if not logLevel:
198            self.log.setLogLevel(None)

Creates an instance of the client.

Args
  • pluginId: ID string of the TouchPortal plugin using this client. Required.
  • sleepPeriod: Seconds to sleep the event loop between socket read events. Default: 0.01
  • autoClose: If True then this client will automatically disconnect when a closePlugin message is received from TP. Default is False.
  • checkPluginId: Validate that pluginId matches ours in any messages from TP which contain one (such as actions). Default is True.
  • updateStatesOnBroadcast: Re-send all cached State values whenever user switches TP page. Default is True.
  • maxWorkers: Maximum worker threads to run concurrently for event handlers. Default of None creates a default-constructed ThreadPoolExecutor.
  • executor: Passed to pyee.ExecutorEventEmitter. By default this is a default-constructed ThreadPoolExecutor, optionally using maxWorkers concurrent threads.
  • useNamespaceCallbacks: use NamespaceCallback as message handler Default is False meaning It will send normal json True meaning It will automatically convert json to namespace to make easier access value eg json: data['actionId']['value'] and namespace would be data.actionId.value
  • loggerName: Optional name for the Logger to be used by the Client. Default of None creates (or uses, if it already exists) the "root" (default) logger.
  • logLevel: Desired minimum logging level, one of: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py logging module Level constants), or None to disable all logging. The level can also be set at runtime via setLogLevel() method. Default is "INFO".
  • logStream: Set a stream to write log messages to, or None to disable stream (console) logging. The stream logger can also be modified at runtime via setLogStream() method. Default is sys.stderr.
  • logFileName: A file name (with optional path) for log messages. Paths are relative to current working directory. Pass None or empty string to disable file logging. The log file is rotated once per day and the last 7 logs are preserved (older ones are deleted). The file logger can also be modified at runtime via setLogFile() method. Default is None (file logging is disabled).
TPHOST = '127.0.0.1'

TP plugin server host IPv4 address.

TPPORT = 12136

TP plugin server host IPv4 port number.

RCV_BUFFER_SZ = 4096

[B] incoming data buffer size.

SND_BUFFER_SZ = 1048576

[B] maximum size of send data buffer (1MB).

SOCK_EVENT_TO = 1.0

[s] maximum wait time for socket events (blocking timeout for selector.select()).

def isConnected(self)
343    def isConnected(self):
344        """
345        Returns `True` if the Client is connected to Touch Portal, `False` otherwise.
346        """
347        return not self.__stopEvent.is_set()

Returns True if the Client is connected to Touch Portal, False otherwise.

def setLogLevel(self, level)
349    def setLogLevel(self, level):
350        """ Sets the minimum logging level. `level` can be one of one of:
351            "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py `logging` module Level constants),
352            or `None` to disable all logging.
353        """
354        self.log.setLogLevel(level)

Sets the minimum logging level. level can be one of one of: "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG" (or equivalent Py logging module Level constants), or None to disable all logging.

def setLogStream(self, stream)
356    def setLogStream(self, stream):
357        """ Set a destination for the StreamHandler logger. `stream` should be a file stream type (eg. os.stderr) or `None` to disable. """
358        self.log.setStreamDestination(stream)

Set a destination for the StreamHandler logger. stream should be a file stream type (eg. os.stderr) or None to disable.

def setLogFile(self, fileName)
360    def setLogFile(self, fileName):
361        """ Set a destination for the File logger. `filename` should be a file name (with or w/out a path) or `None` to disable the file logger. """
362        self.log.setFileDestination(fileName)

Set a destination for the File logger. filename should be a file name (with or w/out a path) or None to disable the file logger.

def isActionBeingHeld(self, actionId: str)
364    def isActionBeingHeld(self, actionId:str):
365        """
366        For an Action with ID of `actionId` that can be held (`hasHoldFunctionality` is true),
367        this method returns `True` while it is being held and `False` otherwise.
368        """
369        return actionId in self.__heldActions

For an Action with ID of actionId that can be held (hasHoldFunctionality is true), this method returns True while it is being held and False otherwise.

def createState( self, stateId: str, description: str, value: str, parentGroup: str = None)
371    def createState(self, stateId:str, description:str, value:str, parentGroup:str = None):
372        """
373        This will create a TP State at runtime. `stateId`, `description`, and `value` (`value` becomes the State's default value) are all required except `parentGroup` which allow you to create states in a `folder`.
374        """
375        if stateId and description and value != None:
376            if stateId not in self.currentStates:
377                self.send({"type": "createState", "id": stateId, "desc": description, "defaultValue": value, "parentGroup": parentGroup})
378                self.currentStates[stateId] = value
379            else:
380                self.stateUpdate(stateId, value)

This will create a TP State at runtime. stateId, description, and value (value becomes the State's default value) are all required except parentGroup which allow you to create states in a folder.

def createStateMany(self, states: list)
382    def createStateMany(self, states:list):
383        """
384        Convenience function to create several States at once. `states` should be an iteratable of `dict` types
385        in the form of `{'id': "StateId", 'desc': "Description", 'value': "Default Value"}` and optionally `'parentGroup': "Value"` which will make state `folder`.
386        """
387        try:
388            for state in states:
389                if isinstance(state, dict):
390                    self.createState(state.get('id', ""), state.get('desc', ""), state.get('value', ""), state.get("parentGroup", ""))
391                else:
392                    self.__raiseException(f'createStateMany() requires a list of dicts, got {type(state)} instead.')
393        except:
394            self.__raiseException(f"createStateMany() requires an iteratable, got {type(states)} instead.")

Convenience function to create several States at once. states should be an iteratable of dict types in the form of {'id': "StateId", 'desc': "Description", 'value': "Default Value"} and optionally 'parentGroup': "Value" which will make state folder.

def removeState(self, stateId: str, validateExists=True)
396    def removeState(self, stateId:str, validateExists = True):
397        """
398        This removes a State that has been created at runtime. `stateId` is the ID to remove.
399        If `validateExists` is True (default) this method will raise an Exception if the `stateId`
400        does not exist in the current state cache.
401        """
402        if stateId and stateId in self.currentStates:
403            self.send({"type": "removeState", "id": stateId})
404            self.currentStates.pop(stateId)
405        elif validateExists:
406            self.__raiseException(f"{stateId} Does not exist.", Exception)

This removes a State that has been created at runtime. stateId is the ID to remove. If validateExists is True (default) this method will raise an Exception if the stateId does not exist in the current state cache.

def removeStateMany(self, states: list)
408    def removeStateMany(self, states:list):
409        """
410        Convenience function to remove several States at once. `states` should be an iteratable of state ID strings.
411        This method does not validate if the states exist in the current state cache. See also: `removeState`.
412        """
413        try:
414            for state in states:
415                self.removeState(state, False)
416        except TypeError:
417            self.__raiseException(f'removeStateMany() requires an iteratable, got {type(states)} instead.')

Convenience function to remove several States at once. states should be an iteratable of state ID strings. This method does not validate if the states exist in the current state cache. See also: removeState.

def choiceUpdate(self, choiceId: str, values: list)
419    def choiceUpdate(self, choiceId:str, values:list):
420        """
421        This updates the list of choices in a previously-declared TP State with id `stateId`.
422        See TP API reference for details on updating list values.
423        """
424        if choiceId:
425            if isinstance(values, list):
426                self.send({"type": "choiceUpdate", "id": choiceId, "value": values})
427                self.choiceUpdateList[choiceId] = values
428            else:
429                self.__raiseException(f'choiceUpdate() values argument needs to be a list not a {type(values)}')

This updates the list of choices in a previously-declared TP State with id stateId. See TP API reference for details on updating list values.

def choiceUpdateSpecific(self, stateId: str, values: list, instanceId: str)
431    def choiceUpdateSpecific(self, stateId:str, values:list, instanceId:str):
432        """
433        This updates a list of choices in a specific TP Item Instance, specified in `instanceId`.
434        See TP API reference for details on updating specific instances.
435        """
436        if stateId and instanceId:
437            if isinstance(values, list):
438                self.send({"type": "choiceUpdate", "id": stateId, "instanceId": instanceId, "value": values})
439            else:
440                self.__raiseException(f'choiceUpdateSpecific() values argument needs to be a list not a {type(values)}')

This updates a list of choices in a specific TP Item Instance, specified in instanceId. See TP API reference for details on updating specific instances.

def settingUpdate(self, settingName: str, settingValue)
442    def settingUpdate(self, settingName:str, settingValue):
443        """
444        This updates a value named `settingName` in your plugin's Settings to `settingValue`.
445        """
446        if settingName and settingName not in self.currentSettings or self.currentSettings[settingName] != settingValue:
447            self.send({"type": "settingUpdate", "name": settingName, "value": settingValue})
448            self.currentSettings[settingName] = settingValue

This updates a value named settingName in your plugin's Settings to settingValue.

def stateUpdate(self, stateId: str, stateValue: str)
450    def stateUpdate(self, stateId:str, stateValue:str):
451        """
452        This updates a value in ether a pre-defined static State or a dynamic State created in runtime.
453        """
454        self.__stateUpdate(stateId, stateValue, False)

This updates a value in ether a pre-defined static State or a dynamic State created in runtime.

def stateUpdateMany(self, states: list)
462    def stateUpdateMany(self, states:list):
463        """
464        Convenience function to update several states at once.
465        `states` should be an iteratable of `dict` types in the form of `{'id': "StateId", 'value': "The New Value"}`.
466        """
467        try:
468            for state in states:
469                if isinstance(state, dict):
470                    self.stateUpdate(state.get('id', ""), state.get('value', ""))
471                else:
472                    self.__raiseException(f'StateUpdateMany() requires a list of dicts, got {type(state)} instead.')
473        except TypeError:
474            self.__raiseException(f"createStateMany() requires an iteratable, got {type(states)} instead.")

Convenience function to update several states at once. states should be an iteratable of dict types in the form of {'id': "StateId", 'value': "The New Value"}.

def showNotification(self, notificationId: str, title: str, msg: str, options: list)
476    def showNotification(self, notificationId:str, title:str, msg:str, options:list):
477        """
478        This method allows your plugin to send a notification to Touch Portal with custom title, message body and available user action(s).
479        Requires TP SDK v4.0 or higher.
480
481        Args:
482            `notificationId`: Unique ID of this notification.
483            `title`: The notification title.
484            `msg`: The message body text that is shown in the notifcation.
485            `options`: List of options (actions) for the notification. Each option should be a `dict` type with `id` and `title` keys.
486        """
487        if notificationId and title and msg and options and isinstance(options, list):
488            for option in options:
489                if 'id' not in option.keys() or 'title' not in option.keys():
490                    self.__raiseException("all options require id and title keys")
491            self.send({
492                "type": "showNotification",
493                "notificationId": str(notificationId),
494                "title": str(title),
495                "msg": str(msg),
496                "options": options
497            })

This method allows your plugin to send a notification to Touch Portal with custom title, message body and available user action(s). Requires TP SDK v4.0 or higher.

Args
  • notificationId: Unique ID of this notification.
  • title: The notification title.
  • msg: The message body text that is shown in the notifcation.
  • options: List of options (actions) for the notification. Each option should be a dict type with id and title keys.
def shortIdUpdate(self, shortId: str, connectorValue: int)
511    def shortIdUpdate(self, shortId:str, connectorValue:int):
512        """
513        This allows you to update slider position value using shortId which TouchPortal will broadcast.
514
515        Args:
516            `shortId`: a shortId is a id that is mapped to connectorId by TouchPortal
517            `connectorValue`: A integer between 0-100
518
519        """
520        if 0 <= connectorValue <= 100:
521            self.send({
522                "type": "connectorUpdate",
523                "shortId": shortId,
524                "value": str(connectorValue)
525            })

This allows you to update slider position value using shortId which TouchPortal will broadcast.

Args
  • shortId: a shortId is a id that is mapped to connectorId by TouchPortal
  • connectorValue: A integer between 0-100
def connectorUpdate(self, connectorId: str, connectorValue: int)
527    def connectorUpdate(self, connectorId:str, connectorValue:int):
528        """
529        This allows you to update slider position value.
530
531        Args:
532            `connectorId`: Cannot be longer then 200 characters.
533                connectorId have syntax that you need to follow https://www.touch-portal.com/api/index.php?section=connectors
534                Also according to that site It requires you to have prefix "pc_yourPluginId_" + connectorid however This already provide
535                you the prefix and the pluginId so you just need you take care the rest eg connectorid|setting1=aValue
536            `connectorValue`: Must be an integer between 0-100.
537
538        Note: This method will automatically looking for shortId using gaven connectorId however if It cannot find shortId it will just send connectorId
539        """
540        if not isinstance(connectorId, str):
541            self.__raiseException(f"connectorId needs to be a str not a {type(connectorId)}")
542        if not isinstance(connectorValue, int):
543            self.__raiseException(f"connectorValue requires a int not {type(connectorValue)}")
544        if 0 <= connectorValue <= 100:
545            if f"pc_{self.pluginId}_{connectorId}" in self.shortIdTracker:
546                self.shortIdUpdate(self.shortIdTracker[connectorId], connectorValue)
547            else:
548                self.send({
549                    "type": "connectorUpdate",
550                    "connectorId": f"pc_{self.pluginId}_{connectorId}",
551                    "value": str(connectorValue)
552                })
553        else:
554            self.__raiseException(f"connectorValue needs to be between 0-100 not {connectorValue}")

This allows you to update slider position value.

Args
  • connectorId: Cannot be longer then 200 characters. connectorId have syntax that you need to follow https://www.touch-portal.com/api/index.php?section=connectors Also according to that site It requires you to have prefix "pc_yourPluginId_" + connectorid however This already provide you the prefix and the pluginId so you just need you take care the rest eg connectorid|setting1=aValue
  • connectorValue: Must be an integer between 0-100.

Note: This method will automatically looking for shortId using gaven connectorId however if It cannot find shortId it will just send connectorId

def updateActionData(self, instanceId: str, stateId: str, minValue, maxValue)
556    def updateActionData(self, instanceId:str, stateId:str, minValue, maxValue):
557        """
558        This allows you to update Action Data in one of your Action. Currently TouchPortal only supports changing the minimum and maximum values in numeric data types.
559        """
560        self.send({"type": "updateActionData", "instanceId": instanceId, "data": {"minValue": minValue, "maxValue": maxValue, "id": stateId, "type": "number"}})

This allows you to update Action Data in one of your Action. Currently TouchPortal only supports changing the minimum and maximum values in numeric data types.

def getChoiceUpdatelist(self)
562    def getChoiceUpdatelist(self):
563        """
564        This will return a dict that `choiceUpdate` registered.
565            example return value `{"choiceUpdateid1": ["item1", "item2", "item3"], "exampleChoiceId": ["Option1", "Option2", "Option3"]}`
566
567        You should use this to verify before Updating the choice list
568        **Note** This is the same as TPClient.choiceUpdateList variable *DO NOT MODIFY* TPClient.choiceUpdateList unless you know what your doing
569        """
570        return self.choiceUpdateList

This will return a dict that choiceUpdate registered. example return value {"choiceUpdateid1": ["item1", "item2", "item3"], "exampleChoiceId": ["Option1", "Option2", "Option3"]}

You should use this to verify before Updating the choice list Note This is the same as TPClient.choiceUpdateList variable DO NOT MODIFY TPClient.choiceUpdateList unless you know what your doing

def getStatelist(self)
572    def getStatelist(self):
573        """
574        This will return a dict that have key pair of states that you last updated.
575            Example retun value `{"stateId1": "value1", "stateId2": "value2", "stateId3": "value3"}`
576        This is used to keep track of all states. It will be automatically updated when you update states
577        **Note** This is the same as TPClient.currentState variable *DO NOT MODIFY* TPClient.currentState unless you know what your doing
578        """
579        return self.currentStates

This will return a dict that have key pair of states that you last updated. Example retun value {"stateId1": "value1", "stateId2": "value2", "stateId3": "value3"} This is used to keep track of all states. It will be automatically updated when you update states Note This is the same as TPClient.currentState variable DO NOT MODIFY TPClient.currentState unless you know what your doing

def getSettinghistory(self)
581    def getSettinghistory(self):
582        """
583        This will return a dict that have key pair of setting value that you updated previously.
584
585        This is used to track settings value that you have updated previously
586        **Note** This is the same as TPClient.currentSettings variable *DO NOT MODIFY* TPClient.currentSettings unless you know what your doing
587        """
588        return self.currentSettings

This will return a dict that have key pair of setting value that you updated previously.

This is used to track settings value that you have updated previously Note This is the same as TPClient.currentSettings variable DO NOT MODIFY TPClient.currentSettings unless you know what your doing

def send(self, data)
590    def send(self, data):
591        """
592        This will try to send any arbitrary Python object in `data` (presumably something `dict`-like) to Touch Portal
593        after serializing it as JSON and adding a `\n`. Normally there is no need to use this method directly, but if the
594        Python API doesn't cover something from the TP API, this could be used instead.
595        """
596        if not self.isConnected():
597            self.__raiseException("TP Client not connected to Touch Portal, cannot send commands.", Exception)
598        if self.__getWriteLock():
599            if len(self.__sendBuffer) + len(data) > self.SND_BUFFER_SZ:
600                self.__writeLock.release()
601                self.__raiseException("TP Client send buffer is full!", ResourceWarning)
602            self.__sendBuffer += (json.dumps(data)+'\n').encode()
603            self.__writeLock.release()
604            self.__dataReadyEvent.set()

This will try to send any arbitrary Python object in data (presumably something dict-like) to Touch Portal after serializing it as JSON and adding a . Normally there is no need to use this method directly, but if the Python API doesn't cover something from the TP API, this could be used instead.

def connect(self)
606    def connect(self):
607        """
608        Initiate connection to TP Server.
609        If successful, it starts the main processing loop of this client.
610        Does nothing if client is already connected.
611
612        **Note** that `connect()` blocks further execution of your script until one of the following occurs:
613          - `disconnect()` is called in an event handler,
614          - TP sends `closePlugin` message and `autoClose` is `True`
615          - or an internal error occurs (for example Touch Portal disconnects unexpectedly)
616        """
617        if not self.isConnected():
618            self.__open()
619            self.send({"type":"pair", "id": self.pluginId})
620            self.__run()  # start the event loop

Initiate connection to TP Server. If successful, it starts the main processing loop of this client. Does nothing if client is already connected.

Note that connect() blocks further execution of your script until one of the following occurs:

  • disconnect() is called in an event handler,
  • TP sends closePlugin message and autoClose is True
  • or an internal error occurs (for example Touch Portal disconnects unexpectedly)
def disconnect(self)
622    def disconnect(self):
623        """
624        This closes the connection to TP and terminates the client processing loop.
625        Does nothing if client is already disconnected.
626        """
627        if self.isConnected():
628            self.__close()

This closes the connection to TP and terminates the client processing loop. Does nothing if client is already disconnected.

@staticmethod
def getActionDataValue(data: list, valueId: str = None)
630    @staticmethod
631    def getActionDataValue(data:list, valueId:str=None):
632        """
633        Utility for processing action messages from TP. For example:
634            {"type": "action", "data": [{ "id": "data object id", "value": "user specified value" }, ...]}
635
636        Returns the `value` with specific `id` from a list of action data,
637        or `None` if the `id` wasn't found. If a null id is passed in `valueId`
638        then the first entry which has a `value` key, if any, will be returned.
639
640        Args:
641            `data`: the "data" array from a TP "action", "on", or "off" message
642            `valueId`: the "id" to look for in `data`. `None` or blank to return the first value found.
643        """
644        if not data: return None
645        if valueId:
646            return next((x.get('value') for x in data if x.get('id', '') == valueId), None)
647        return next((x.get('value') for x in data if x.get('value') != None), None)

Utility for processing action messages from TP. For example: {"type": "action", "data": [{ "id": "data object id", "value": "user specified value" }, ...]}

Returns the value with specific id from a list of action data, or None if the id wasn't found. If a null id is passed in valueId then the first entry which has a value key, if any, will be returned.

Args
  • data: the "data" array from a TP "action", "on", or "off" message
  • valueId: the "id" to look for in data. None or blank to return the first value found.
Inherited Members
pyee.executor.ExecutorEventEmitter
shutdown
pyee.base.EventEmitter
on
listens_to
add_listener
event_names
emit
once
remove_listener
remove_all_listeners
listeners
class TYPES:
35class TYPES:
36    """
37    Event type enumerations. These correspond to the message types Touch Portal may send.
38    Event handler callbacks receive one parameter, which is the JSON data structure sent
39    from TP and then converted to a corresponding Python object using `json.loads()`.
40    Usage example:
41        ```py
42        import TouchPortalAPI as TP
43
44        client = TP.Client("myplugin")
45
46        @client.on(TP.TYPES.onConnect)
47        # equivalent to: @client.on('info')
48        def onConnect(data):
49            print(data)
50
51        # alternatively, w/out decorators:
52        def onAction(data):
53            print(data)
54
55        client.on(TP.TYPES.onAction, onAction)
56        ```
57    """
58    onHold_up = 'up'
59    """ When the action is used in a hold functionality and the user releases the Touch Portal button (Api 3.0+) """
60    onHold_down = 'down'
61    """ When the action is used in a hold functionality and the user presses the Touch Portal button down (Api 3.0+) """
62    onConnect = 'info'
63    """ Info message returned after successful pairing. """
64    onAction = 'action'
65    """ When actions are triggered by the user (Api 1.0+) """
66    onListChange = 'listChange'
67    """ When a user makes a change in one of your lists (Api 1.0+) """
68    onConnectorChange = 'connectorChange'
69    """ When a connector is used in a slider functionality (connector) (Api 4.0+) """
70    onShutdown = 'closePlugin'
71    """ When Touch Portal tries to close your plug-in (Api 2.0+) """
72    onBroadcast = 'broadcast'
73    """ When Touch Portal broadcasts a message (Api 3.0+) """
74    onSettingUpdate = 'settings'
75    """ When the plugin's Settings have been updated (by user or from the plugin itself) (Api 3.0+) """
76    onNotificationOptionClicked = "notificationOptionClicked"
77    """ When a user clicks on a notification action (Api 4.0+) """
78    shortConnectorIdNotification = 'shortConnectorIdNotification'
79    """ When creating new connector for the first time It will generate shortid and It will send as an event (Api 5.0+) """
80    allMessage = 'any'
81    """ Special event handler which will receive **all** messages from TouchPortal. """
82    onError = 'error'
83    """ Special event emitted when any other event callback raises an exception.
84        For this particular event, the parameter passed to the callback handler will be an `Exception` object.
85        See also `pyee.ExecutorEventEmitter.error` event.
86    """

Event type enumerations. These correspond to the message types Touch Portal may send. Event handler callbacks receive one parameter, which is the JSON data structure sent from TP and then converted to a corresponding Python object using json.loads().

Usage example
import TouchPortalAPI as TP

client = TP.Client("myplugin")

@client.on(TP.TYPES.onConnect)
# equivalent to: @client.on('info')
def onConnect(data):
    print(data)

# alternatively, w/out decorators:
def onAction(data):
    print(data)

client.on(TP.TYPES.onAction, onAction)
TYPES()
onHold_up = 'up'

When the action is used in a hold functionality and the user releases the Touch Portal button (Api 3.0+)

onHold_down = 'down'

When the action is used in a hold functionality and the user presses the Touch Portal button down (Api 3.0+)

onConnect = 'info'

Info message returned after successful pairing.

onAction = 'action'

When actions are triggered by the user (Api 1.0+)

onListChange = 'listChange'

When a user makes a change in one of your lists (Api 1.0+)

onConnectorChange = 'connectorChange'

When a connector is used in a slider functionality (connector) (Api 4.0+)

onShutdown = 'closePlugin'

When Touch Portal tries to close your plug-in (Api 2.0+)

onBroadcast = 'broadcast'

When Touch Portal broadcasts a message (Api 3.0+)

onSettingUpdate = 'settings'

When the plugin's Settings have been updated (by user or from the plugin itself) (Api 3.0+)

onNotificationOptionClicked = 'notificationOptionClicked'

When a user clicks on a notification action (Api 4.0+)

shortConnectorIdNotification = 'shortConnectorIdNotification'

When creating new connector for the first time It will generate shortid and It will send as an event (Api 5.0+)

allMessage = 'any'

Special event handler which will receive all messages from TouchPortal.

onError = 'error'

Special event emitted when any other event callback raises an exception. For this particular event, the parameter passed to the callback handler will be an Exception object. See also pyee.ExecutorEventEmitter.error event.