Modbussniffer: How to access external component from another external component

Hello,
I have here a heating system for my house with a modbus master device (Windhager heating) and one slave (controller for heatexchanger pumps etc). I connected an ESP in parallel (I don’t want to change the existing installation) and my goal is to listen on the bus and catch the slaves values which are exchanged. In a second step I will also catch the data coming from the master.

I got inspired by following topic: Modbus "Man-in-the-middle" But it was not exactly fitting my needs.

Finally I ended up copying the ESPHome Modbus component and rename it to ModbusSnifferComponent. There I succesfully altered the method parse_modbus_byte_() so that I’m able to decode the byte stream from the uart into request and response frames. My next step would be to call
device->on_modbus_data(data); in order to save the data.

For this I copied the ESP home device implementation ModbusController and renamed it to MyModbusController. I had to do this, because the constructor of this class expects an object of type Modbus which I don’t have. In my case it’s ModbusSniffer.

And now my problem:
In __init___.py of my_modbus_controller I need to import ModbusSniffer. But as far as I understand it is not possible to import an external component to an external component. I tried something like this, but without success:
from .my_components import ModbusSniffer
I’m stuck now. Any hints tipps ideas what to do?

Below some extracts of my code…

my folder structure:

esphome-web-8edf90.yaml
my_components/
  modbus_sniffer/
    __init__.py
    modbus_sniffer.cpp
    modbus_sniffer.h
  my_modbus_controller/
    __init__.py
    my_modbus_controller.cpp
    my_modbus_controller.h
    const.py
    (and other files)

esphome-web-8edf90.yaml extract:

external_components:
  - source:
      type: local
      path: ./my_components

uart:
  - id: mod_bus
    #(more parameters)

modbus_sniffer: #works fine
  id: modbus_sniff
  uart_id: mod_bus

my_modbus_controller:
- id: modbus_device
  address: 0x3C
  modbus_id: modbus_sniff  #this handover is the challenge
  setup_priority: -10

my_components/modbus_sniffer/_init_.py

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID

DEPENDENCIES = ["uart"]

modbus_sniffer_component_ns = cg.esphome_ns.namespace("modbus_sniffer_component")
ModbusSnifferComponent = modbus_sniffer_component_ns.class_(
    "ModbusSnifferComponent", cg.Component, uart.UARTDevice
)

CONFIG_SCHEMA = (
    cv.Schema(
        {
            cv.GenerateID(): cv.declare_id(ModbusSnifferComponent),
        }
    )
    .extend(cv.COMPONENT_SCHEMA)
    .extend(uart.UART_DEVICE_SCHEMA)
)


async def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])
    await cg.register_component(var, config)
    await uart.register_uart_device(var, config)

my_components/modbus_sniffer/modbus_sniffer_component.h:

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"

namespace esphome {
namespace modbus_sniffer_component {


class ModbusDevice;

class ModbusSnifferComponent : public uart::UARTDevice, public Component {
  public:
    ModbusSnifferComponent() = default;
    void setup() override;
    void loop() override;
    void dump_config() override;
    void register_device(ModbusDevice *device) { this->devices_.push_back(device); }
    void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
    
  protected:
    bool parse_modbus_byte_(uint8_t byte);
    bool disable_crc_;
    std::vector<uint8_t> rx_buffer_;
    uint32_t last_modbus_byte_{0};
    std::vector<ModbusDevice *> devices_;
};

class ModbusSnifferDevice {
 public:
  explicit ModbusSnifferDevice(uint8_t address) : address_(address) {}
  void set_parent(ModbusSnifferComponent *parent) { parent_ = parent; }
  void set_address(uint8_t address) { address_ = address; }
  virtual void on_modbus_data(const std::vector<uint8_t> &data) = 0;
  virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
  virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){};
 
 protected:
  friend ModbusSnifferComponent;
 
  ModbusSnifferComponent *parent_;
  uint8_t address_;
};

}  // namespace modbus_sniffer_component
}  // namespace esphome

my_components/my_modbus_controller/_init_.py extract:

import binascii

from esphome import automation
import esphome.codegen as cg
from .my_components import ModbusSniffer //<------ here is currently my problem
import esphome.config_validation as cv
from esphome.const import (
    CONF_ADDRESS,
    CONF_ID,
    CONF_LAMBDA,
    CONF_NAME,
    CONF_OFFSET,
    CONF_TRIGGER_ID,
)
from esphome.cpp_helpers import logging

from .const import (
    CONF_ALLOW_DUPLICATE_COMMANDS,
    CONF_BITMASK,
    CONF_BYTE_OFFSET,
    CONF_COMMAND_THROTTLE,
    CONF_CUSTOM_COMMAND,
    CONF_FORCE_NEW_RANGE,
    CONF_MAX_CMD_RETRIES,
    CONF_MODBUS_CONTROLLER_ID,
    CONF_OFFLINE_SKIP_UPDATES,
    CONF_ON_COMMAND_SENT,
    CONF_ON_OFFLINE,
    CONF_ON_ONLINE,
    CONF_REGISTER_COUNT,
    CONF_REGISTER_TYPE,
    CONF_RESPONSE_SIZE,
    CONF_SKIP_UPDATES,
    CONF_VALUE_TYPE,
)

CODEOWNERS = ["@martgras"]

AUTO_LOAD = ["modbus_sniffer_component"]

CONF_READ_LAMBDA = "read_lambda"
CONF_SERVER_REGISTERS = "server_registers"
MULTI_CONF = True

modbus_controller_ns = cg.esphome_ns.namespace("my_modbus_controller")
ModbusController = modbus_controller_ns.class_(
    "MyModbusController", cg.PollingComponent, modbus_sniffer_component.ModbusDevice
)

....

my_modbus_controller/my_modbus_controller.h

#pragma once

#include "esphome/core/component.h"

#include "esphome/my_components/modbus_sniffer/modbus_sniffer_component.h"
#include "esphome/core/automation.h"

#include <list>
#include <queue>
#include <set>
#include <utility>
#include <vector>

namespace esphome {
namespace my_modbus_controller {

class MyModbusController;

(some code)

class MyModbusController : public PollingComponent, public modbus_sniffer_component::ModbusDevice { //<--- here I had to exchange "modbus" by "modbus_sniffer_component"
   (more code)
}

}  // namespace my_modbus_controller
}  // namespace esphome

Regards

P.S.: I was not able to figure out what to do that the .h extracts are highlighted in color. I tried to mark it as C++, C, Python but no success.

Hi - if one of the Dev types doesn’t see this, you may have more luck finding them on the Discord server: ESPHome

Hi zoogara,

thanks for the hint to ask at Discord. They were able to help me there. The solution is to rename my component from ModbusSnifferComponent to ModbusSniffer (in order to have the same name as the subfolder where the component lies) and use below line in _init_.py of the MyModbusController to import modbus_sniffer:

from esphome.components import modbus_sniffer