Modbus with multiple sensors on same bus tagging the sensors

Hello to all,

I’m “relatively” new into telegraf and influxdb. Yesterday I came across a new feature that I wanted to try out right away [[inputs.modbus.request]]. I using Telegraf 1.22.0 and Influx CLI 2.2.1.

I have a three modbus sensors on the same serial controller. All the sensors are the same but for the question I have this shouldn’t matter. My first idea what to name the sensors with a unique name in some way (in my case three different names for the three modbus sensors), but I can’t get this to work. That’s why I study further in the documenten of telegrad and influx and I found out that you can also tag items.

In the end I decided to tag the modbus sensors with a unique name. Below you can see the result.

My question: Is this solution to your view a good solution or can this be solved in a better/neater way that I am not aware of yet?

[[outputs.influxdb_v2]]
  urls = ["http://127.0.0.1:8086"]
  token = "<random>"
  organization = "<random>"
  bucket = "db_sdm630"
  namepass = ["SDM630"]
  
# ------------------------------------------------ Inputs -------------------------------------------- 

[[inputs.modbus]]
  interval = "5s"
  name_override = "SDM630"
  name = "NOT USED" # Not used
  tagexclude = ["type", "name", "host"]
  timeout = "2s"

  ## Serial (RS485; RS232)
  controller = "file:///dev/ttyUSB-MODBUS"
  baud_rate = 115200
  data_bits = 8
  parity = "E"
  stop_bits = 1
  transmission_mode = "RTU"
  configuration_type = "request"

# ---------------------------------------------- slave_id = 1 ---------------------------------------- 
[[inputs.modbus.request]]
  slave_id = 1
  byte_order = "ABCD"
  register = "input"
  fields = [
      { address = 0,    name = "Phase 1 line to neutral volts",          type="FLOAT32" },
      { address = 2,    name = "Phase 2 line to neutral volts",          type="FLOAT32" },
      { address = 4,    name = "Phase 3 line to neutral volts",          type="FLOAT32" },
    ]
  [inputs.modbus.request.tags]
    sensor = "pv"

# ---------------------------------------------- slave_id = 2 ---------------------------------------- 
[[inputs.modbus.request]]
  slave_id = 2
  byte_order = "ABCD"
  register = "input"
  fields = [
      { address = 0,    name = "Phase 1 line to neutral volts",          type="FLOAT32" },
      { address = 2,    name = "Phase 2 line to neutral volts",          type="FLOAT32" },
      { address = 4,    name = "Phase 3 line to neutral volts",          type="FLOAT32" },
    ]
  [inputs.modbus.request.tags]
    sensor = "heat_pump_outdoors"
    
# ---------------------------------------------- slave_id = 2 ---------------------------------------- 
[[inputs.modbus.request]]
  slave_id = 3
  byte_order = "ABCD"
  register = "input"
  fields = [
      { address = 0,    name = "Phase 1 line to neutral volts",          type="FLOAT32" },
      { address = 2,    name = "Phase 2 line to neutral volts",          type="FLOAT32" },
      { address = 4,    name = "Phase 3 line to neutral volts",          type="FLOAT32" },
    ]
  [inputs.modbus.request.tags]
    sensor = "heat_pump_indoors"

Hi @neptunus,
Your solution is great and is exactly what tags are defined for. This will work well for looking at your data from a more general abstract level (all sensor data). It will also provide you with the scope to effectively drill down to look at more specific sensor data.

Always keep in mind cardinality when using tags: Resolve high series cardinality | InfluxDB Cloud Documentation. However, for your use, I do not see you reaching any limitations. Your Telegraf config looks extremely clean as well, the only comment I would make is for usability defining shorter fields names (“Phase 1 line to neutral volts”) helps with more advanced queries.

Thank for the feedback!

I still have something where I make a mistake and don’t know how to solve it.

I was able to get the e.g. [[inputs.cpu]] and [[inputs.disk]] template running in telegraf.conf. But when i try to add another .conf file, to receive data stream from somthing else onto a specific bucket, one of the buckets gets populated with the Measurements from the wrong output.

What I don’t understand is why is data from “[[outputs.influxdb_v2]]” from sdm630.conf ending up in bucket “db_home” and “db_sdm630” and why is the data from [[outputs.influxdb_v2]] solaredge_modbus.conf only added in bucket = “db_solaredge”?
I would like to get the data from the different configs in the associated buckets.

It would be great if someone could help me. Below my configs.

The main is /etc/telegraf/telegraf.conf

[global_tags]
[agent]
  interval = "10s"
  round_interval = true
  metric_batch_size = 1000
  metric_buffer_limit = 10000
  collection_jitter = "0s"
  flush_interval = "10s"
  flush_jitter = "0s"
  precision = ""
  hostname = ""
  omit_hostname = false
 
[[outputs.influxdb_v2]]
  urls = ["http://127.0.0.1:8086"]
  token = "<random>"
  organization = "<random>"
  bucket = "db_home"

[[inputs.cpu]]
  percpu = true
  totalcpu = true
  collect_cpu_time = false
  report_active = false

[[inputs.disk]]
  

Then i have the linux one /etc/telegraf/telegraf.d/sdm630.conf:

[[outputs.influxdb_v2]]
  urls = ["http://127.0.0.1:8086"]
  token = "<random>"
  organization = "<random>"
  bucket = "db_sdm630"
  namepass = ["SDM630"]
  
# ------------------------------------------------ Inputs -------------------------------------------- 

[[inputs.modbus]]
  interval = "5s"
  name_override = "SDM630"
  name = "NOT USED" # Not used
  tagexclude = ["type", "name", "host"]
  timeout = "2s"

  ## Serial (RS485; RS232)
  controller = "file:///dev/ttyUSB-MODBUS"
  baud_rate = 115200
  data_bits = 8
  parity = "E"
  stop_bits = 1
  transmission_mode = "RTU"
  configuration_type = "request"

# ---------------------------------------------- slave_id = 1 ---------------------------------------- 
[[inputs.modbus.request]]
  slave_id = 1
  byte_order = "ABCD"
  register = "input"
  fields = [
      { address = 0,    name = "Phase 1 line to neutral volts",          type="FLOAT32" },
      { address = 2,    name = "Phase 2 line to neutral volts",          type="FLOAT32" },
      { address = 4,    name = "Phase 3 line to neutral volts",          type="FLOAT32" },
    ]
  [inputs.modbus.request.tags]
    sensor = "pv"

# ---------------------------------------------- slave_id = 2 ---------------------------------------- 
[[inputs.modbus.request]]
  slave_id = 2
  byte_order = "ABCD"
  register = "input"
  fields = [
      { address = 0,    name = "Phase 1 line to neutral volts",          type="FLOAT32" },
      { address = 2,    name = "Phase 2 line to neutral volts",          type="FLOAT32" },
      { address = 4,    name = "Phase 3 line to neutral volts",          type="FLOAT32" },
    ]
  [inputs.modbus.request.tags]
    sensor = "heat_pump_outdoors"
    
# ---------------------------------------------- slave_id = 3 ---------------------------------------- 
[[inputs.modbus.request]]
  slave_id = 3
  byte_order = "ABCD"
  register = "input"
  fields = [
      { address = 0,    name = "Phase 1 line to neutral volts",          type="FLOAT32" },
      { address = 2,    name = "Phase 2 line to neutral volts",          type="FLOAT32" },
      { address = 4,    name = "Phase 3 line to neutral volts",          type="FLOAT32" },
    ]
  [inputs.modbus.request.tags]
    sensor = "heat_pump_indoors"

I also have /etc/telegraf/telegraf.d/solaredge_modbus.conf:

[[outputs.influxdb_v2]]
  urls = ["http://127.0.0.1:8086"]
  token = "<random>"
  organization = "<random>"
  bucket = "db_solaredge"
  namepass = ["inverter"]
  
# ------------------------------------------------ Inputs -------------------------------------------- 

[[inputs.modbus]]
 interval = "15s"
 name_override="inverter"
 name = "NOT USED" # Not used
 tagexclude = ["type", "name", "host"]

 slave_id = 1
 timeout = "5s"
 controller = "tcp://xxx.xxx.xxx.xxx:502"

 # Note: most static data is omitted (pointless to monitor)
 # Note: battery and meter data is omitted (as i have none)
 #
 # Always 0/not used: 
 #  { name = "I_Status_Vendor", byte_order = "AB", data_type = "UINT16", scale=1.0,  address = [108]} 

 # Disabled for now since Telegraf does not support reading strings

 # { name = "c_serialnumber", address = [52, 67]},

 holding_registers = [
    { name = "C_SunSpec_DID",     byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [69]},
    { name = "I_AC_Current",      byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [71]},
    { name = "I_AC_CurrentA",     byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [72]},
    { name = "I_AC_CurrentB",     byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [73]},
    { name = "I_AC_CurrentC",     byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [74]},
    { name = "I_AC_Current_SF",   byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [75]},
    { name = "I_AC_VoltageAB",    byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [76]},
    { name = "I_AC_VoltageBC",    byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [77]},
    { name = "I_AC_VoltageCA",    byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [78]},
    { name = "I_AC_VoltageAN" ,   byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [79]},
    { name = "I_AC_VoltageBN",    byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [80]},
    { name = "I_AC_VoltageCN",    byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [81]},
    { name = "I_AC_Voltage_SF",   byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [82]},
    { name = "I_AC_Power",        byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [83]},
    { name = "I_AC_Power_SF",     byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [84]},
    { name = "I_AC_Frequency",    byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [85]},
    { name = "I_AC_Frequency_SF", byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [86]},
    { name = "I_AC_VA",           byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [87]},
    { name = "I_AC_VA_SF",        byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [88]},
    { name = "I_AC_VAR",          byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [89]},
    { name = "I_AC_VAR_SF",       byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [90]},
    { name = "I_AC_PF",           byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [91]},
    { name = "I_AC_PF_SF",        byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [92]},
    { name = "I_AC_Energy_WH",    byte_order = "ABCD", data_type = "INT32",  scale=1.0, address = [93, 94]},
    { name = "I_AC_Energy_WH_SF", byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [95]},
    { name = "I_DC_Current",      byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [96]},
    { name = "I_DC_Current_SF",   byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [97]},
    { name = "I_DC_Voltage",      byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [98]},
    { name = "I_DC_Voltage_SF",   byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [99]},
    { name = "I_DC_Power",        byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [100]},
    { name = "I_DC_Power_SF",     byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [101]},
    { name = "I_Temp_Sink",       byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [103]},
    { name = "I_Temp_SF",         byte_order = "AB",   data_type = "INT16",  scale=1.0, address = [106]},
    { name = "I_Status",          byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [107]},
    { name = "I_Status_Vendor",   byte_order = "AB",   data_type = "UINT16", scale=1.0, address = [108]}
  ]

  # Must come last because Telegraf
  [inputs.modbus.tags]
   site = "<random>" # Site ID can be found on SolarEdge's website
   sn = "<random>" # Inverter serial

# Apply the scaling and drop the scaling fields.
[[processors.starlark]]
  namepass = ["inverter"]
  source = '''
def scale(metric, name):
        metric.fields[name] *= pow(metric.fields[name + "_SF"])
        metric.fields.pop(name + "_SF")

def pow(exp):
        # It works i suppose
        return float("1e{}".format(exp))

def drop(metric, name):
	metric.fields.pop(name)
	metric.fields.pop(name + "_SF")

def apply(metric):
	type = metric.fields["C_SunSpec_DID"]
	metric.fields.pop("C_SunSpec_DID")

        I_AC_Voltage_Scale = pow(metric.fields["I_AC_Voltage_SF"])
        metric.fields["I_AC_VoltageAB"] *= I_AC_Voltage_Scale
        metric.fields["I_AC_VoltageBC"] *= I_AC_Voltage_Scale
        metric.fields["I_AC_VoltageCA"] *= I_AC_Voltage_Scale
        metric.fields["I_AC_VoltageAN"] *= I_AC_Voltage_Scale
        metric.fields["I_AC_VoltageBN"] *= I_AC_Voltage_Scale
        metric.fields["I_AC_VoltageCN"] *= I_AC_Voltage_Scale
        metric.fields.pop("I_AC_Voltage_SF")

	# Drop meaningless measurements
	if type == 101: # Single Phase
		metric.fields.pop("I_AC_VoltageBC")
		metric.fields.pop("I_AC_VoltageCA")
		metric.fields.pop("I_AC_VoltageAN")
		metric.fields.pop("I_AC_VoltageBN")
		metric.fields.pop("I_AC_VoltageCN")
	elif type == 102: # Split Phase
		metric.fields.pop("I_AC_VoltageCA")
		metric.fields.pop("I_AC_VoltageCN")
	
        scale(metric, "I_AC_Frequency")
	scale(metric, "I_DC_Voltage")
	scale(metric, "I_Temp")

	# Drop obsolete measurements at night/sleep mode to reduce stored data size.
	if metric.fields["I_Status"] == 2:
		drop(metric, "I_AC_Current")
		metric.fields.pop("I_AC_CurrentA")
		metric.fields.pop("I_AC_CurrentB")
		metric.fields.pop("I_AC_CurrentC")
	
		drop(metric, "I_AC_Power")		
		drop(metric, "I_AC_VA")
		drop(metric, "I_AC_VAR")
		drop(metric, "I_AC_PF")
		drop(metric, "I_AC_Energy_WH")
		drop(metric, "I_DC_Current")
		drop(metric, "I_DC_Power")
	else:
		I_AC_Current_Scale = pow(metric.fields["I_AC_Current_SF"])
		metric.fields["I_AC_Current"] *= I_AC_Current_Scale
		metric.fields["I_AC_CurrentA"] *= I_AC_Current_Scale
		metric.fields["I_AC_CurrentB"] *= I_AC_Current_Scale
		metric.fields["I_AC_CurrentC"] *= I_AC_Current_Scale
		metric.fields.pop("I_AC_Current_SF")
		
		# Drop obsolete measurments
		if type == 101: # Single Phase
			metric.fields.pop("I_AC_CurrentB")
			metric.fields.pop("I_AC_CurrentC")
		elif type == 102: # Split Phase
			metric.fields.pop("I_AC_CurrentC")

		scale(metric, "I_AC_Power")
		scale(metric, "I_AC_VA")
		scale(metric, "I_AC_VAR")
		scale(metric, "I_AC_PF")
		scale(metric, "I_AC_Energy_WH")
		scale(metric, "I_DC_Current")
		scale(metric, "I_DC_Power")

	# Convert serial to tag
	#metric.tags["sn"] = metric.fields["C_SerialNumber"]
	#metric.fields.pop("C_SerialNumber")

	# Correct the type, we multiply by float but still some are reported as int (for reasons unkown).
	for k, v in metric.fields.items():
		if k != "I_Status":
			metric.fields[k] = float(v)

	return metric
'''

a
b
c

Hey @neptunus,
So I assume, db_home within your config is db_hound within your screenshot. To stop data from measurement entering db_hount you will need to deploy a namedrop like so:

[global_tags]
[agent]
  interval = "10s"
  round_interval = true
  metric_batch_size = 1000
  metric_buffer_limit = 10000
  collection_jitter = "0s"
  flush_interval = "10s"
  flush_jitter = "0s"
  precision = ""
  hostname = ""
  omit_hostname = false
 
[[outputs.influxdb_v2]]
  namedrop = ["SDM630"]
  urls = ["http://127.0.0.1:8086"]
  token = "<random>"
  organization = "<random>"
  bucket = "db_home"

[[inputs.cpu]]
  percpu = true
  totalcpu = true
  collect_cpu_time = false
  report_active = false

Yes you are right. I did a C&P mistake in the post. Luckily not in my config. :innocent:

After reading some more documentation yesterday if fixed my issue with:

namepass = ["cpu", "disk", "diskio", "kernel", "mem", "processes", "swap", "system"]

namedrop would also have worked. Thanks for the hint

I still have one question: Why is the data from [[outputs.influxdb_v2]] solaredge_modbus.conf only added in bucket = “db_solaredge” and not in bucket = “db_hound” ? Don’t misunderstand, this is what I want as behavior. But I don’t understand the difference with sdm630.conf.

Just want to learn. :smirk: