-
Notifications
You must be signed in to change notification settings - Fork 4
30. tuyaDAEMON as event processor
With version 2, tuyaDAEMON reaches its maturity and now it is possible to try to come up with some more formal definitions.
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:
-
Proactively (PUSH): In response to internal or external changes (e.g., timers, sensors, switches, ...).
-
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:
-
Stores the last value in
global.tuyastatus
, a Node-RED object. -
Displays real-time information in the Node-RED UI's debug pad.
-
Publishes MQTT messages to a local broker.
-
Logs events to the
tuyathome.messages
database table. -
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.
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>
}
}}}
-
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 <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>)
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:
- undef meets 2 cases:
value === undefined
, or attributemissed
. - _ Only in some user's input (e.g.
do_command
,do_logging
subflows) 'NULL' => undef._ - _if
remote
undef =>local instance
, else requires thecore_SYSTEM
module installed. GET
is always without side effectsSET
can have side effects (used as a trigger) and a dummy value (any).
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:
- "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.
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
.
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)]
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)}]
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
andmsg.infodp
, that identify the provenience task.
-
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. -
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.
- 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
.
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.