Downlink handling

LoRaWAN devices can receive downlinks either at any time (Class C) or as a response to an uplink. A downlink can be a command from the LNS, some simple bytes like 0x0123 or complex multipart downlinks. Using Home Assistant or any other software to control devices you can not just predefine the downlinks but need to generate them on the fly. energy2mqtt allows all forms of downlinks from predefined in the configuration up to generatation by a tool or shell script.

Downlinks can be specified for payload_on, payload_open, payload_close and payload_off in the configuration path of sensors, binary_sensors and valve. More complex stuff like fan or climate handle the downlink generation by setting up command topics and subscribing to those.

If you want to have a downlink send for the payload_ functions you have two options:

  • !Basic tag allows to set a hex or ascii string to be published to the LNS
  • !Complex allows setting all needed aspects of a downlink and is much more complex.

The downlinks are prebuild by energy2mqtt and packed into a base64 string which will be sent by Home Assistant to energy2mqtt when you click on the entity on the UI.

The basic downlinks are sent as unconfirmed frame to frame port 1 with the immediately set to false. So only use that way of downlink generation if your device accepts all those parameters.

Complex downlinks are mapped to a Rust struct and converted to the correct format for the different LNS systems. energy2mqtt allows you to set most parameters of the downlink with the !Complex YAML tag.

The following example sets all available settings:

  payload_close: !Complex { data: "8854CEFF", port: 66, imme: True, confirmed: False }

data needs to be hex encoded and will be converted to the correct data format by the code of the implementation of the LNS connector. Refer to the device documentation to find the correct port for the LoRaWAN message. Complex Downlinks require you to set that port, there is no default. imme defaults to false because most LoRaWAN devices are Class A devices. confirmed also defaults to true to ensure that the downlinks is answered and the LNS can verify the correct reception.

Some devices require complex downlinks to enable different features of a device bases on the last values received or combine different parameters, e.g. if you set the temperature of the AC you need to send the “activate devices” flags together with the temperature.

The relevant platforms like fan or climate define which commands are acceptable. You specify those parts in the configuration. Lets look at this example for a climate device:

  - name: climate_control
    platform: climate
    friendly_name: My perfect Climate Control

    # our current temperature
    # Hint: current_temperature_topic will be set to our main topic
    temperature_command_topic: set_temp
    current_temperature_template: "{{ value_json.temperature }}"

    # The target temperature
    # Hint: temperature_state_topic will be set to our main topic
    temperature_state_template: "{{ value_json.target_temperature }}"    

    # Hint: mode_state_topic will be set to our main topic
    mode_command_topic: set_mode
    mode_state_template: "{{ ## }}"
    modes:
      - fan_only
      - heat
      - cool

Looking at that example you can see two command topic definitions temperature_command_topic and mode_command_topic. You must not set a complete MQTT topic but only set a single word. That word like set_temp and set_mode will be the first parameter of the tool to call.

The tool to call

Within energy2mqtt we use YAML definition to find the correct way to understand the parser output of the LoRaWAN payload. The tool to generate the downlink must be within the same directory and needs to be named like the YAML file but with the file extension .command instead of .yaml. That file can be anything from shell scripts to nodejs scripts or a C / Rust binary as long as your system is able to run it and the executable flag is set in the filesystem. That is the main reason while the defs/ directory can not be mounted as nonexec.

Having the option to run the a command does not only require the information about which command_topic was called but also the current state of the sensor and the data Home Assistant sent us. This data is encoded in the environment of the process. The following environment variables are always set:

  • PAYLOAD: the data sent by home assistant. Like ON and OFF or the temperature of the climate to set
  • DEVICE: the id of the device. For LoRaWAN devices that is the DevEUI
  • DEV_: those variables contains the current state of the sensor.

A minimal script for the AC my look like this (see the Milesight WT303 command for an extended version):

#!/bin/sh
set -e

# default values
payload=""
imme=true
port=85

## the command_topic set_mode is set
if [ "x$1" = "xset_mode" ]; then

    MODE=0 # default is fan_only
    if [ "x$PAYLOAD" = "xheat" ]; then
        MODE=1
    elif [ "x$PAYLOAD" = "xcool" ]; then
        MODE=2
    fi

    payload=`printf "68%02X" $MODE`

# set_temp needs to check for the mode (heating or cooling)
elif [ "x$1" = "xset_temp" ]; then

    # Linux shells did not work with float values so utilize tools like jq
    TEMPERATURE=`jq -n $PAYLOAD*100`

    if [ $TEMPERATURE < 500 -o $TEMPERATURE > 3800 ]; then
        echo "Temperature out of scope 5 < $PAYLOAD < 38" >&2
        exit 1
    fi

    # At this point we use the last value of temperature_control_mode
    # of the sensor. 
    MODE="6B"
    if [ "x$DEV_TEMPERATURE_CONTROL_MODE" = "xcooling" ]; then
        MODE="6C"
    fi

    # Endianess needs to be handled correctly if device and
    # Linux host differ.
    endian=`printf %04X $TEMPERATURE`
    high=`echo $endian | cut -b 1-2`
    low=`echo $endian | cut -b 3-4`

    payload=`printf "%s%s%s" $MODE $low $high`
else
    # Return your errors to stderr to be displayed in the log of energy2mqtt
    echo -n "command $1 not allowed with payload $PAYLOAD" >&2
    exit 1
fi

# Return the values needed
echo -n "{ \"proto\": \"lora\", \"id\": \"$DEVICE\", \"imme\": $imme, \"port\": $port, \"payload\": \"$payload\" }"
exit 0