Skip to content

30. tuyaDAEMON as event processor

Marco Sillano edited this page Dec 19, 2023 · 14 revisions

With version 2, tuyaDAEMON reaches its maturity and now it is possible to try to come up with some more formal definitions.

devices, tasks, events

A TuyaDAEMON device is a self-contained entity that can execute one or more tasks, which are defined either by the system's design or by the user, as specified in the alldevices structure: https://github.com/msillano/tuyaDAEMON/wiki/40.-tuyaDAEMOM-global.alldevices. Each task is identified by a combination of two names or IDs:

             task := <device>.<property>

The only constraint on a device is that it must be accessible and controllable using Node-RED.

TuyaDAEMON encompasses a wide range of devices, including functions integrated with the system's operating system (e.g., text-to-speech, timers, executables, etc...), extensions developed for TuyaDAEMON (e.g., core_trigger and core_OpenAPI), as well as IoT sensors and actuators.

Any task can generate events, which can be triggered in two ways:

  1. Proactively (PUSH): In response to internal or external changes (e.g., timers, sensors, switches, ...).

  2. As a result of executing a command (SET, GET, SCHEMA, etc.) initiated by the user (directly or using an APP) or by TuyaDAEMON itself.

In addition to producing an event, a task can also have side effects, which represent the desired outcome in the physical world (e.g., Switch.on => ON, side effect: turns on the light).

The Node-RED implementation of tuyaDEAMON.core is asynchronous and event-driven. To summarize: TuyaDAEMON serves as an event processor that generates many standard outputs from any event:

  1. Stores the last value in global.tuyastatus, a Node-RED object.

  2. Displays real-time information in the Node-RED UI's debug pad.

  3. Publishes MQTT messages to a local broker.

  4. Logs events to the tuyathome.messages database table.

  5. Generates new events.

Note: Since version 2.2.0, users can filter outputs based on device/property.

REF: core internal event format, used by the user only to access event data in a 'share':

            msg: {
                 remote:  undef|<remote-name>
                 from:    <deviceCid>|<deviceID>
                 infodp:  <pseudoDP>!<dp>
                 info: {
                       device:   <usr-dev-name>|<deviceCid>|<deviceId>    // user-defined name or ID
                       property: <usr-dp-name>|<pseudoDP>|<dp>            // user-defined name or DP
                       value:    <decoded-value>
                       }
                 share: <share-object>                          // optional, a copy from 'alldevices'
                 }

note: the device's and property's user-names (for internationalization), used in public messages and in global.tuyastaus, can be defined in global.alldevices by the user. Otherwise, the native "id" and "dp" are used.

note: pseudoDPs, internally defined, allow the handling of some special operations (e.g. REFRESH, _standby, ...) as standard commands.

note: 'remote' requires the core_SYSTEM module installed.

native event format

REF: native event format (from 'tuya-smart-device' node, modified) used by any device, real, virtual and fake. The default for remote_from is 'local'. This msg is accepted by core.logging INPUT node:

             msg: { 
                 payload:{
                    remote_from: <remote_name>|undef               // TuyaDEAMON extension, optional
                    deviceId:    <deviceId>|<gatewayId>      // <gatewayId> only for virtual devices 
                    deviceName:  <deviceName>|undef       // from 'tuya-smart-device' node, not used 
                    data: {
                          t:    <timestamp>|undef                       // timestamp (sec), optional 
                          cid:  <deviceCid>|undef                        // only for virtual devices
                          dps:{
                             <dp>: <value>          // <value> can be encoded (Tuya devices) or JSON
                              ... more ...                               // one or more <dp>:<value>
                              }
                }}}

commands

  • A command fires a task process, using an optional <value>. The task execution can:

    • in general: anything you can do programming in node-red.

In case of IOT devices (see tuya-devices behavior):

- SET a single _device status attribute_ to a &lt;value> (called also 'Data Points' or **'dp'** or **'code'** in the Tuya documentation). 
- GET a single 'dp' value or the SCHEMA (i.e. all dp:value pairs) from a _device_
- force the internal update of one or more 'dp' (REFRESH), and get the changed values 
- trigger operations in a _device_ (only SET, e.g. [WiFi_IP_camera.'start SD format'](https://github.com/msillano/tuyaDAEMON/blob/main/devices/WiFi_IP_Camera/device_WiFi_IP_Camera.pdf)) 
  • A command is an msg that defines: the tuyaDAEMON server remote (default locale), the task, and, if required, some input data (value).
          command := {<remote>|(<remote>, )<device>(, <property>(, <value>))}
  • where useful, a command can also be written in functional form:

         command := <device>.<property>(<value>)
    
  • or in the short declaration form, when the 'device' is defined by the context (e.g. in OO methods):

          command := <property>(<param-type-list>|<param-name-list>)         
    

standard tuyaDAEMON command

REF: Public standard tuyaDAEMON command message format. This command_msg is accepted by core.std_cmds and core.fast_cmds INPUT nodes:

         command_msg: { 
              payload:{
                     remote:   undef|<remote-name>                            // from global.remotemap, optional
                     device:   undef|<usr-dev-name>|<deviceCid>|<deviceId>       // mandatory if `remote` missed 
                     property: undef|<usr-dp-name>|<pseudoDP>|<dp>           // mandatory if `value` not `undef`
                     value:    undef|<any> 
                     }
               } 

TuyaDAEMON command mapping to device

   if device = undef and remote = undef  => _not allowed (ERROR)_
      elseif device = undef  and remote valid => _GET remote devices list_ (tuyaDAEMON extension)
         elseif property = undef  => _GET device SCHEMA_
            elseif value = undef  => _GET device property_
               else  => _SET device property_

note:

  1. undef meets 2 cases: value === undefined, or attribute missed.
  2. _ Only in some user's input (e.g. do_command, do_logging subflows) 'NULL' => undef._
  3. _if remote undef => local instance, else requires the core_SYSTEM module installed.
  4. GET is always without side effects
  5. SET can have side effects (used as a trigger) and a dummy value (any).

normalized command

REF: Rich internal normalized format. This msg is also sent to custom 'fake' devices, via core.Fake_cmds_OUT node.

            msg: { 
                 to:      <deviceCid>|<deviceId>                                        // used as key
                 toDev:   <gatewayId>|<deviceId>             // to select the 'tuya-smart-device' node (if exists)
                 infodp:  <pseudoDP>|<dp>                                               // used as key
                 hide:    undef|<codestr>                        // since v.2.2.0, optional visibility
                 payload:{
                       device:   <usr-dev-name>|<msg.to>                        // used with tuyastats
                       property: undef|<usr-dp-name>|<msg.infodp>               // used with tuyastats
                       value:    undef|<any>                       
                       }
                 }

REF: Native real devices command format, modified from 'tuya-smart-device'

single SET command:

            msg: { 
               toDev:   <gatewayId>|<deviceId>        // to select the device (tuyaDAEMON extension)
               payload:{
                    cid : <deviceCid>                                   // only for virtual devices 
                    dps :  <dp>     
                    set:   <value>     
                    }
                }  

note:

  1. "cid" identifies a subdevice when "toDev" == gatewayId, see TuyAPI docs. You can use tuya-cli to get Id, Key, and CID.

For native GET, SCHEMA, MULTIPLE, etc. see CORE.low_lev_IN node info.

share and command chains

The share structure was born to allow the building of derived devices from base devices in OO style.

But the share has proven so powerful that it has become the key to meta-programming in the tuyaDAEMON environment, offering users the possibility to define any algorithm (share implements sequences, selections, and iterations: hence it satisfies the assumptions of the Böhm-Jacopini theorem) without node-red use (tuyaDAMON chains).

                   tuyaDAEMON(share(<event>)) => <commands> 

A more concise definition: a 'share' conditionally transforms an 'event' into one or more 'commands'.

               share := [(test1 && test2 && ...)?[command2(), command3(), ...], ...]

               share(<event1>) := [ if (<test1>(<event1>, tuyastatus) && <test2>(<event1>, tuyastatus) &&...)
                                         do [<command2>(<event1>, tuyastatus), <command3>(<event1>, tuyastatus)...]
                                    if ... more...]

In a share: testX, command.property, and command.value can be defined using the js 'eval()' on a user-defined js code fragment. Because both the event and tuyastatus are in the visibility of the evaluated fragment, we can affirm that test, command.property and command.value all are user function(event, tuyastatus)

REF: The 'share' format is simple: see alldevices documentation for details.

note: in a share, if command.device, command.property, command.value are missed the value comes from the caller event (i.e. msg.info.xxxx) by inheritance, to simplify the share's code. To force undefined (as required by GET, SCHEMA, etc.) you have to explicitly set it to null.

sequences and fork

The default for undef or null test is true, i.e. 'do command'. The following share will start unconditionally 'command2' after 'event1':

               share(<event1>) := (if (empty)) do <command2>(<event1>, tuyastatus)

EXMP: The command2.value can be set from event.value, like in the example:

               "share": [{ "action": [{
                                "device":   "<device2>",
                                "property": "<foo>",
                                "value":    "@msg.info.value"
                                 }]}]        // the starting '@' signals the use of `eval()`

    //   or, using inheritance, in shorter form:

               "share": [{ "action": [{
                                "device":   "<device2>",
                                "property": "<foo>"                            
                                 }]}]       

This share acts like a pipe: the device2.foo receives as input the previous event.value (it is in msg.info.value) doing a sequence of filters.

EXMP: This hoter share acts like a fork, genarating two chains working in parallel on node-red:

             share(<event1>) := (if (empty)) do [<command2>(<event1>, tuyastatus), <command3>(<event1>, tuyastatus)]

selection

It is simple to build shares that act like if...then...else or like switch...case...case:

            share(<event1>) := [{ if (event1.value  > n) do <command2>(<event1>, tuyastatus)},
                                { if (event1.value <= n) do <command3>(<event1>, tuyastatus)}]

iteration

Using recursion it is natural to implement shares that act like 'for(){...}' or while() do{...}. See the following example:


EXMP: We want a function that repeats a beep() N times, with the T [ms] period.

In pseudo-code, we can write a recursive function:

                          repeatBeep(N,T) := 
                                      if (N <= 0), done
                                      fork beep()
                                      delay(T)
                                      call repeatBeep(N-1, T)

To implement it in the tuyaDAEMON environment, using the _system._beep task, we must define in global.alldevices: a new task (e.g. _beep_loop) for a device (e.g. _system), without code implementation. Like so:
note: _timerON send a command delayed, syntax _timerON(delay, command).

                 _beep_loop(count, timeout) := share[(count > 0)?[_beep(), _timerON(timeout, _beep_loop(count -1, timeout)]]

In alldevices, this is the share code (using a big 'eval()' for value):

                {
                "dp":          "_beep_loop",
                "name":        "my first beep loop",
                "capability":  "SKIP",                           // SKIP: the device (_system) is not called
                "share": [{
                        "test": [ "msg.info.value.count > 0"],        // until count > 0, 'test' uses eval()
                        "action": [
                               {"device":    "_system",                          // fork 1:  `_system._beep`
                                "property":  "_beep",
                                "value":     "any" },
                               {"device":    "_system",                       // fork 2:  `_system._timerON`
                                "property":  "_timerON",         //_timerON value is a timeout and a command
                                "value":     "@ _xy = {                                      // a big eval()
                                      timeout: msg.info.value.timeout,                        // timer delay
                                      alarmPayload:{          // after delay, does this 'command' for itself
                                           device:   '_system', 
                                           property: '_beep_loop',
                                           value: {
                                                count:   (msg.info.value.count -1),          // with count-1
                                                timeout: msg.info.value.timeout            // and same delay
                                                }}}; _xy "  
                                  }]         // the construct "@_xy ={...};_xy" is required by the `eval()`                                    
                          }]
                 },

Alternative implementation, using 'dynamic' objects:

                   "share": [
                        {
                            "test": ["msg.info.value.count > 0"],
                            "action": [
                                {   "property": "_beep",
                                    "value": 1 },
                                {   "property": "_timerON",
                                    "value": {
                                        "timeout": "@msg.info.value.timeout",
                                        "alarmPayload": {
                                            "device": "_system",
                                            "property": "_beep_loop",
                                            "value": {
                                                "count": "@msg.info.value.count -1",
                                                "timeout": "@msg.info.value.timeout"
                                            }}}
                                } ]
                        } ]

The user can edit a share directly in node-red, modifying 'alldevices' (in the 'Global CORE config' node), or use the forms of tuyaDAEMON things tool developed to simplify the management offline of 'alldevices'.

Warning: because of the alldevices implementation, a task can have only one share. This reduces the reusability of tasks when a task must be used by more than one chain. Workarounds:

  • using a test, it is possible to distinguish between the different chains, e.g. using msg.from and msg.infodp, that identify the provenience task.

tuyaDEAMON chains and 'custom' devices

  1. User can build a new "custom"|"fake" device by writing a node-red flow to implement device behavior and DPs processing, using standard msg as input/output.

  2. User can 'merge' a "real" device with a 'custom' flow that implements new properties (DPs), see tuya_bridge.

  • The flow simply filters incoming messages and processes new ones, sending others to the device.
  • In global.alldevices, you need to merge the new DPs definitions to the "real" device DPs.
  1. User can also add new 'custom' properties to a device, without an ad-hoc flow, but only using 'tuyaDAEMON chains' defined in global.alldevices. To do this define a new DP like this:
                    {
                    "dp": "_testPing24H",
                    "capability": "SKIP",
                    "share": [ ... ]
                    }

The 'SKIP' capability causes 'share' to execute immediately.

  • Take care to produce an event message at the end, if required.
  • See examples: tuya_bridge._testPing24H, core._beep_loop.

features

tuyaDAEMON 2.X is a comprehensive open IoT framework that provides users with a powerful event processor called the CORE and various extensions and tools to build their own IoT projects. It offers a unified approach to handling all types of devices, including Tuya-Smart, virtual, custom, software-only, and hardware devices.

The framework's structure is straightforward:

1. User Responsibilities:

  • Deployment and Management: The user is responsible for setting up and managing the TuyaDAEMON server.

  • Device Definition: The user must define all devices, including their names, data points (DPs), allowed values, quirks, etc... within the global.alldevices structure.

  • App Development: The user creates a vertical application over TuyaDAEMON using various tools such as Node-RED, databases, HTML, REST APIs, MQTT, etc., to achieve their specific goals.

2. TuyaDAEMON Responsibilities:

  • Event and Command Handling: TuyaDAEMON processes any command or event as defined in the global.alldevices structure. It also verifies all commands to ensure smooth and reliable IoT operations.

3. User-Defined Device Functionality:

  • Task Chaining: Users can build complex behaviors by chaining existing tasks together. This meta-programming approach allows for 'Turing-complete' functionality.

  • Device Derivation: Users can create new devices by deriving them from existing ones. This involves adding new tasks or DPs to existing devices.

  • Object-Oriented Device Design: Users can design new devices by composing them from two or more existing devices, creating a modular and reusable approach.

  • Custom Device Integration: Users can integrate any non-Tuya hardware devices using a Node-RED bridge flows.

  • Custom Hardware and Software Devices: Users can build custom hardware devices or software-only devices to cater to specific areas of interest.

4. User Interface Development:

  • Node-RED Apps: Users can create Node-RED applications to interact with TuyaDAEMON devices and manage their functionalities.

  • REST Interface: Users can utilize the REST interface (HTTP) to build PHP applications like tuyaDAEMON.toolkit, which can test new devices through REST calls.

  • MQTT and Database Access: Users can leverage MQTT and database access to create custom applications and user interfaces.

In summary, tuyaDAEMON 2.X empowers users to build versatile and scalable IoT solutions by providing a flexible, extensible, and neutral framework.

Clone this wiki locally