# -------------------------------------------------
# MESHTASTIC
# Your node uplinks everything it hears as JSON to msh/EU_868/England/2/json/LongFast/<node>.
# Each packet's "sender" is the uplinking node (yours) and "from" is the node that sent it, so a
# packet is your node's own when "!%08x" of from equals sender. Other nodes' telemetry is ignored
# for your node's sensors, so they never show someone else's battery.
# If your node uses a different region or root topic, change the 5 "topic:" lines.
# -------------------------------------------------

# Your node's own device telemetry: battery, voltage, channel use, uptime.
- trigger:
    - platform: mqtt
      topic: "msh/EU_868/England/2/json/LongFast/+"
  condition:
    - condition: template
      value_template: >
        {% set p = trigger.payload_json %}
        {{ p is mapping and p.type == 'telemetry' and p.from is defined and p.sender is defined
           and ('!%08x' | format(p.from | int)) == p.sender
           and p.payload is mapping and p.payload.voltage is defined }}
  sensor:
    - name: "Meshtastic Battery Voltage"
      unique_id: meshtastic_own_battery_voltage
      device_class: voltage
      state_class: measurement
      unit_of_measurement: "V"
      state: >
        {% set v = (trigger.payload_json | default({})).payload | default({}) %}
        {{ v.voltage | float | round(2) if v.voltage is defined else this.state }}
    - name: "Meshtastic Battery Percent"
      unique_id: meshtastic_own_battery_percent
      device_class: battery
      state_class: measurement
      unit_of_measurement: "%"
      # Meshtastic reports 101 when it's running on external power.
      state: >
        {% set v = (trigger.payload_json | default({})).payload | default({}) %}
        {{ [v.battery_level | int, 100] | min if v.battery_level is defined else this.state }}
      attributes:
        external_power: "{{ (((trigger.payload_json | default({})).payload | default({})).battery_level | default(0) | int) > 100 }}"
    - name: "Meshtastic Channel Utilisation"
      unique_id: meshtastic_own_channel_utilisation
      state_class: measurement
      unit_of_measurement: "%"
      icon: mdi:radio-tower
      state: >
        {% set v = (trigger.payload_json | default({})).payload | default({}) %}
        {{ v.channel_utilization | float | round(1) if v.channel_utilization is defined else this.state }}
    - name: "Meshtastic Air Util TX"
      unique_id: meshtastic_own_air_util_tx
      state_class: measurement
      unit_of_measurement: "%"
      icon: mdi:access-point
      state: >
        {% set v = (trigger.payload_json | default({})).payload | default({}) %}
        {{ v.air_util_tx | float | round(2) if v.air_util_tx is defined else this.state }}
    - name: "Meshtastic Uptime"
      unique_id: meshtastic_own_uptime
      device_class: duration
      unit_of_measurement: "h"
      state: >
        {% set v = (trigger.payload_json | default({})).payload | default({}) %}
        {{ (v.uptime_seconds | int / 3600) | round(1) if v.uptime_seconds is defined else this.state }}

# When anything last came through from your node: the uplink is working.
- trigger:
    - platform: mqtt
      topic: "msh/EU_868/England/2/json/LongFast/+"
  sensor:
    - name: "Meshtastic Last Packet"
      unique_id: meshtastic_last_packet
      device_class: timestamp
      icon: mdi:access-point-network
      state: "{{ now().isoformat() }}"
      attributes:
        type: "{{ trigger.payload_json.type | default('') }}"

# Names of every node ever heard (from nodeinfo packets), so messages show a name, not a number.
- trigger:
    - platform: mqtt
      topic: "msh/EU_868/England/2/json/LongFast/+"
  condition:
    - condition: template
      value_template: >
        {% set p = trigger.payload_json %}
        {{ p is mapping and p.type == 'nodeinfo' and p.from is defined and p.payload is mapping }}
  sensor:
    - name: "Meshtastic Node Registry"
      unique_id: meshtastic_node_registry
      icon: mdi:account-network
      state: "{{ (state_attr('sensor.meshtastic_node_registry', 'nodes') or {}) | count }}"
      attributes:
        nodes: >
          {% set p = trigger.payload_json %}
          {% set old = state_attr('sensor.meshtastic_node_registry', 'nodes') or {} %}
          {% set name = p.payload.longname | default(p.payload.shortname | default(p.payload.id | default(p.from))) %}
          {{ dict(old, **{(p.from | string): name}) }}

# Nodes heard in the last 24 hours: name, hardware, when, hops and the last hop's signal (into your node).
- trigger:
    - platform: mqtt
      topic: "msh/EU_868/England/2/json/LongFast/+"
  condition:
    - condition: template
      value_template: "{{ trigger.payload_json is mapping and trigger.payload_json.from is defined }}"
  sensor:
    - name: "Meshtastic Nodes Heard"
      unique_id: meshtastic_nodes_heard
      icon: mdi:radar
      unit_of_measurement: "nodes"
      state_class: measurement
      state: >
        {% set old = state_attr('sensor.meshtastic_nodes_heard', 'nodes') or {} %}
        {% set cutoff = as_timestamp(now()) - 86400 %}
        {% set ns = namespace(n=0) %}
        {% for k, v in old.items() if v is mapping and v.last > cutoff and k != (trigger.payload_json.from | string) %}
          {% set ns.n = ns.n + 1 %}
        {% endfor %}
        {{ ns.n + 1 }}
      attributes:
        nodes: >
          {% set p = trigger.payload_json %}
          {% set t = as_timestamp(now()) | int %}
          {% set id = p.from | string %}
          {% set old = state_attr('sensor.meshtastic_nodes_heard', 'nodes') or {} %}
          {% set prev = old.get(id) if old.get(id) is mapping else {} %}
          {% set names = state_attr('sensor.meshtastic_node_registry', 'nodes') or {} %}
          {% set info = p.payload if p.type == 'nodeinfo' and p.payload is mapping else {} %}
          {% set rec = {
               'name': info.longname | default(names.get(id, prev.name | default('!%08x' | format(p.from | int)))),
               'hw': info.hardware | default(prev.hw | default(none)),
               'last': t,
               'hops': p.hops_away | default(prev.hops | default(none)),
               'snr': p.snr | default(prev.snr | default(none)),
               'rssi': p.rssi | default(prev.rssi | default(none)),
             } %}
          {% set ns = namespace(out={}) %}
          {% for k, v in old.items() if v is mapping and v.last > t - 86400 %}
            {% set ns.out = dict(ns.out, **{k: v}) %}
          {% endfor %}
          {{ dict(ns.out, **{id: rec}) }}

# Text messages on LongFast: the latest one, and the last 20 with time and sender.
- trigger:
    - platform: mqtt
      topic: "msh/EU_868/England/2/json/LongFast/+"
  condition:
    - condition: template
      value_template: >
        {% set p = trigger.payload_json %}
        {{ p is mapping and p.type == 'text' and p.payload is mapping and p.payload.text is defined }}
  sensor:
    - name: "Meshtastic Last Message"
      unique_id: meshtastic_last_message
      icon: mdi:message-text
      state: >
        {% set p = trigger.payload_json %}
        {% set sender = (state_attr('sensor.meshtastic_node_registry', 'nodes') or {}).get(p.from | string, '!%08x' | format(p.from | int)) %}
        {{ (sender ~ ': ' ~ p.payload.text)[:250] }}
      attributes:
        sender: >
          {{ (state_attr('sensor.meshtastic_node_registry', 'nodes') or {}).get(trigger.payload_json.from | string, '!%08x' | format(trigger.payload_json.from | int)) }}
        message: "{{ trigger.payload_json.payload.text }}"
    - name: "Meshtastic Chat Log"
      unique_id: meshtastic_chat_log
      icon: mdi:message-badge
      # State: when the last message arrived (the messages themselves are in the attribute).
      state: "{{ now().strftime('%d %b %H:%M') }}"
      attributes:
        messages: >
          {% set p = trigger.payload_json %}
          {% set sender = (state_attr('sensor.meshtastic_node_registry', 'nodes') or {}).get(p.from | string, '!%08x' | format(p.from | int)) %}
          {% set old = state_attr('sensor.meshtastic_chat_log', 'messages') %}
          {% set old = old if old is list else [] %}
          {{ ([{'time': now().strftime('%d %b %H:%M'), 'from': sender, 'hops': p.hops_away | default(none),
                'text': p.payload.text[:200]}] + old)[:20] }}
