Automatic TASMOTA RSSI sensors

First off, I know sensors for TASMOTA devices’ RSSI values automatically show up when you use MQTT discovery. But I had never used it, and I didn’t want to start now, after having nearly 30 devices already deployed. I needed a way to dynamically generate my RSSI sensors so that I didn’t have to keep specifying them manually in my YAML. I came up with the following automation:

The trigger is super simple, just match the default state telemetry topic of TASMOTA devices:

automation:
  - alias: publish_rssi
    trigger:
      - platform: mqtt
        topic: 'tele/+/STATE'

I had set the teleperiod to 30 seconds on few of my devices that had sensors, so I was getting barraged with this automation running. I limited the RSSI sensors to one update every 5 minutes.

    # Don't update an RSSI sensor more than once every 5 minutes.
    condition:
      - condition: template
        value_template: >
          {%- set device_name = trigger.topic[5:-6] -%}
          {%- set sensor_state = states.sensor[(device_name | lower) + '_rssi'] -%}
          {%- if not sensor_state -%}
            True
          {%- else -%}
            {%- set range = 5 * 60 - 1 -%}
            {%- set since_last_update = as_timestamp(now()) - as_timestamp(sensor_state.last_updated) -%}
            {{ since_last_update >= range }}
          {%- endif -%}

Finally, the action step, I call a python script to set the state of a sensor, the name of which I construct using the MQTT topic that the device published:

    action:
      - service: python_script.set_state
        data_template:
          overwrite: false
          entity_id: >
            {%- set device_name = trigger.topic[5:-6] -%}
            sensor.{{ device_name | lower }}_rssi
          state: >
            {{ trigger.payload_json.Wifi.RSSI }}
          attributes:
            unit_of_measurement: '%'
            friendly_name: >
              {%- set device_name = trigger.topic[5:-6] -%}
              {{ device_name }}
            mqtt_topic: >
              {%- set device_name = trigger.topic[5:-6] -%}
              {{ device_name }}
            uptime: >
              {{ trigger.payload_json.Uptime }}
            access_point: >
              {%- set access_points = {
                "xx:xx:xx:xx:xx:xx": "hallway",
                "yy:yy:yy:yy:yy:yy": "kitchen",
                "zz:zz:zz:zz:zz:zz": "recroom"
              } -%}
              {{ access_points[trigger.payload_json.Wifi.BSSId] }}
            rssi: >
              {{ trigger.payload_json.Wifi.RSSI | int }}
            channel: >
              {{ trigger.payload_json.Wifi.Channel }}

Lastly the python script that I use to set the state of the entities. It’s pretty useful. You can set the state and any of the attributes of an entity, and by specifying the overwrite flag, you can optionally blank out the state and start from scratch, or just modify and append to existing attributes.

#=====================================
#  python_scripts/set_state.py
#=====================================

entityId = data.get('entity_id')
if entityId is None:
    logger.warning("set_state.py: entity_id not specified.")
else:
    overwrite = data.get('overwrite')
    overwrite = overwrite if (overwrite) else False

    entityState = hass.states.get(entityId)

    newState = data.get('state')
    if newState is None:
        newState = 'unknown' if (overwrite or (not entityState.state)) else entityState.state

    newAttributes = {} if (overwrite or (not entityState.attributes)) else entityState.attributes.copy()
    dataAttributes = data.get('attributes')
    if dataAttributes is None:
        dataAttributes = {}
    for item in dataAttributes:
        newAttributes[item] = dataAttributes[item]

    hass.states.set(entityId, newState, newAttributes)

Here’s Mark II of this automated sensor config. I have automatically-generated sensors like this for each of my tasmota devices:

First, clear out the sensor’s information if we detect a tasmota device has just come online:

automation:
  - alias: tasmota_reboot
    trigger:
      - platform: mqtt
        topic: 'tele/+/INFO1'
    action:
      - service: python_script.set_state
        data_template:
          overwrite: true
          entity_id: >
            {%- set sensor_name = 'tasmota_' + (trigger.topic.split('/')[1] | lower) -%}
            {{ states.sensor[sensor_name].entity_id }}
          state: 'unknown'
          attributes:
            startup_time: >
              {{ as_timestamp(now()) | timestamp_local }}

Next is basically the automation from before which is triggered by the STATE topic, but with some new conditions, and firing an event at the end:

  - alias: tasmota_rssi
    trigger:
      - platform: mqtt
        topic: 'tele/+/STATE'
    # Don't update an RSSI sensor more than once every 5 minutes
    # unless we need to collect other information
    condition:
      - condition: template
        value_template: >
          {%- set device_name = trigger.topic.split('/')[1] -%}
          {%- set sensor = states.sensor['tasmota_' + (device_name | lower)] -%}
          {%- if (not sensor) 
              or ('rssi'            not in sensor.attributes) 
              or ('tasmota_version' not in sensor.attributes)
              or ('ip_address'      not in sensor.attributes)
              or ('mqtt_topic'      not in sensor.attributes)
              or ('group_topic'     not in sensor.attributes)
          -%}
            True
          {%- else -%}
            {%- set range = 5 * 60 - 1 -%}
            {%- set since_last_update = as_timestamp(now()) - as_timestamp(sensor.last_updated) -%}
            {{ since_last_update >= range }}
          {%- endif -%}
    action:
      - service: python_script.set_state
        data_template:
          overwrite: false
          entity_id: >
            {%- set device_name = trigger.topic.split('/')[1] -%}
            sensor.tasmota_{{ device_name | lower }}
          state: >
            {{ trigger.payload_json.Wifi.RSSI }}
          attributes:
            unit_of_measurement: '%'
            updated: >
              {{ as_timestamp(now()) | timestamp_local }}
            access_point: >
              {%- set access_points = {
                "[MAC_ADDRESS_HALLWAY]": "hallway",
                "[MAC_ADDRESS_KITCHEN]": "kitchen",
                "[MAC_ADDRESS_RECROOM]": "recroom"
              } -%}
              {{ access_points[trigger.payload_json.Wifi.BSSId] }}
            rssi: >
              {{ trigger.payload_json.Wifi.RSSI | int }}
            channel: >
              {{ trigger.payload_json.Wifi.Channel }}
      - event: gather_tasmota_info
        event_data_template:
          mqtt_topic: >
            {%- set device_name = trigger.topic.split('/')[1] -%}
            {{ device_name }}

Originally, instead of this event, I just published an MQTT message asking for all the statuses that TASMOTA can send out (i.e., the STATUS 0 command), but the responses came too fast and the respective automations tried to update the state of the sensors too quickly, and some information didn’t make it in. I split it up into multiple automations and this seems to work. Each automation has a condition so that it only fires if we need to collect its particular status information.

The friendly name and MQTT topic:

  - alias: tasmota_status_trigger
    trigger:
      - platform: event
        event_type: gather_tasmota_info
    condition:
      - condition: template
        value_template: >
          {%- set device_name = trigger.event.data.mqtt_topic -%}
          {%- set sensor = states.sensor['tasmota_' + (device_name | lower)] -%}
          {{ 'mqtt_topic' not in sensor.attributes }}
    action:
      - service: mqtt.publish
        data_template:
          topic: >
            {%- set device_name = trigger.event.data.mqtt_topic -%}
            cmnd/{{ device_name }}/Status

  - alias: tasmota_status
    trigger:
      - platform: mqtt
        topic: 'stat/+/STATUS'
    action:
      - service: python_script.set_state
        data_template:
          overwrite: false
          entity_id: >
            {%- set device_name = trigger.topic.split('/')[1] -%}
            sensor.tasmota_{{ device_name | lower }}
          attributes:
            friendly_name: >
              {{ trigger.payload_json.Status.FriendlyName[0] }}
            mqtt_topic: >
              {{ trigger.payload_json.Status.Topic }}

The group topic, OTA URL and startup time

  - alias: tasmota_status1_trigger
    trigger:
      - platform: event
        event_type: gather_tasmota_info
    condition:
      - condition: template
        value_template: >
          {%- set device_name = trigger.event.data.mqtt_topic -%}
          {%- set sensor = states.sensor['tasmota_' + (device_name | lower)] -%}
          {{ 'group_topic' not in sensor.attributes }}
    action:
      - service: mqtt.publish
        data_template:
          topic: >
            {%- set device_name = trigger.event.data.mqtt_topic -%}
            cmnd/{{ device_name }}/Status
          payload: 1

  - alias: tasmota_status1
    trigger:
      - platform: mqtt
        topic: 'stat/+/STATUS1'
    action:
      - service: python_script.set_state
        data_template:
          overwrite: false
          entity_id: >
            {%- set device_name = trigger.topic.split('/')[1] -%}
            sensor.tasmota_{{ device_name | lower }}
          attributes:
            group_topic: "{{ trigger.payload_json.StatusPRM.GroupTopic }}"
            ota_url: "{{ trigger.payload_json.StatusPRM.OtaUrl }}"
            startup_time: "{{ as_timestamp(trigger.payload_json.StatusPRM.StartupUTC + '-00:00') | timestamp_local }}"

The tasmota and library versions:

  - alias: tasmota_status2_trigger
    trigger:
      - platform: event
        event_type: gather_tasmota_info
    condition:
      - condition: template
        value_template: >
          {%- set device_name = trigger.event.data.mqtt_topic -%}
          {%- set sensor = states.sensor['tasmota_' + (device_name | lower)] -%}
          {{ 'tasmota_version' not in sensor.attributes }}
    action:
      - service: mqtt.publish
        data_template:
          topic: >
            {%- set device_name = trigger.event.data.mqtt_topic -%}
            cmnd/{{ device_name }}/Status
          payload: 2

  - alias: tasmota_status2
    trigger:
      - platform: mqtt
        topic: 'stat/+/STATUS2'
    action:
      - service: python_script.set_state
        data_template:
          overwrite: false
          entity_id: >
            {%- set device_name = trigger.topic.split('/')[1] -%}
            sensor.tasmota_{{ device_name | lower }}
          attributes:
            tasmota_version: "{{ trigger.payload_json.StatusFWR.Version }}"
            tasmota_core: "{{ trigger.payload_json.StatusFWR.Core }}"
            tasmota_sdk: "{{ trigger.payload_json.StatusFWR.SDK }}"

And finally, the IP and MAC addresses:

  - alias: tasmota_status5_trigger
    trigger:
      - platform: event
        event_type: gather_tasmota_info
    condition:
      - condition: template
        value_template: >
          {%- set device_name = trigger.event.data.mqtt_topic -%}
          {%- set sensor = states.sensor['tasmota_' + (device_name | lower)] -%}
          {{ 'mac_address' not in sensor.attributes }}
    action:
      - service: mqtt.publish
        data_template:
          topic: >
            {%- set device_name = trigger.event.data.mqtt_topic -%}
            cmnd/{{ device_name }}/Status
          payload: 5

  - alias: tasmota_status5
    trigger:
      - platform: mqtt
        topic: 'stat/+/STATUS5'
    action:
      - service: python_script.set_state
        data_template:
          overwrite: false
          entity_id: >
            {%- set device_name = trigger.topic.split('/')[1] -%}
            sensor.tasmota_{{ device_name | lower }}
          attributes:
            ip_address: "{{ trigger.payload_json.StatusNET.IPAddress }}"
            mac_address: "{{ trigger.payload_json.StatusNET.Mac }}" 

I also found a small bug in my set_state script where specifying overwrite: false didn’t work because it was treating false as a string and not a boolean value.

#==================================================================================================
#  python_scripts/set_state.py
#==================================================================================================

entityId = data.get('entity_id')
if entityId is None:
    logger.warning("set_state.py: entity_id not specified.")
else:
    overwrite = data.get('overwrite')
    if (overwrite is None) or (overwrite.lower() == 'false'):
      overwrite = False
      # logger.debug("overwrite = False")
    else:
      overwrite = True
      # logger.debug("overwrite = True")

    entityState = hass.states.get(entityId)

    newState = data.get('state')
    if newState is None:
        newState = 'unknown' if (overwrite or (not entityState.state)) else entityState.state

    newAttributes = {} if (overwrite or (not entityState.attributes)) else entityState.attributes.copy()
    dataAttributes = data.get('attributes')
    if dataAttributes is None:
        dataAttributes = {}
    for item in dataAttributes:
        newAttributes[item] = dataAttributes[item]

    hass.states.set(entityId, newState, newAttributes)

Ive followed everything here but im not able to get it to work, copy pasted your exact code. Is there anything i need to fill in with my own details to get this to work?

Im just starting out in HA so still learning.

Ha. You sure picked a doozy of a thing to start with :slight_smile:

Are you using the automatic discovery features of TASMOTA? Because I think the MQTT topics will look quite a bit different than the ones I have used here. I have manually configured all my of TASMOTA devices, and I use a similar topic scheme in all of them. That’s part of how this all works.

I get this error :

2020-05-23 08:23:21 ERROR (SyncWorker_6) [homeassistant.components.python_script.set_state.py] Error executing script: ‘NoneType’ object is not callable

on this line :

if (overwrite is None) or (overwrite.lower() == 'false'):

Im not using automatic discovery. I also add them manually, my topics for my devices are like sonoff/light for example. Not tasmota/light. What is the topics you are using to get this script to work, is there anything that I need to fill in with my own data in your automation to get the script to run.

I noticed that I couldn’t make HA pad my Python script an actual boolean value, so I am just checking the content of the string. What version of Python are you on? I’m using 3.8. this is not something that I ever expected to fall… I’m def. not a Python dev.

I don’t know the version of python, just the one in the HA 0.110.0 container. Is there a way to check ?

It just uses the topics to name the generated sensors:
It will issue the cmnd/[topic]/STATUS command with various payloads, and then listen for the corresponding stat/[topic]/STATUS response in order to set the information from the response JSON payload into the sensor’s attributes with the Python set state script.

There should be nothing that you need to change if you’re using those style of topics.

Probably 3.7 then but I don’t think that should make a difference. I could be wrong.