Ability to add multiple modbus hubs

yes, it would be nice. I have several modbus tcp and encapsulated rtu devices.
Is there support for encapsulated rtu?

1 Like

Any chance that this gets implemented?
I tried the code above, it is possible to create “platform: modbus 2” without errors, but unable to attach a sensor to it: “Platform not found: sensor.modbus 2”

It will be great to add more than one tcp devices and be able to configure the relative switch/sensor components.

1 Like

Has anyone made any progress on this? I desperately need a second modbus tcp device added. I tried creating a custom component platform and sensor called modbus2 with changes to the modbus.py. Original files were renamed modbus2.py and mentions to modbus were changed to modbus2, but I ran out of luck…

Alright, so I really needed a second ModBus device data. I decided the easiest route would be to run a separate instance of HA in Docker, have that read the ModBus values from my Sunny Boy, then publish MQTT messages with the values that I need.
Here is the configuration of the HA instance used to do the above:

homeassistant:
  name: Sunny Boy Sensors
  latitude: !secret home_latitude
  longitude: !secret home_longitude
  elevation: !secret home_elevation
  unit_system: metric
  time_zone: !secret home_time_zone
  customize: !include customize.yaml
  customize_glob: !include customize_global.yaml
config:
#frontend:
mqtt:
  broker: !secret mqtt_url
  port: !secret mqtt_port
modbus:
  type: tcp
  host: !secret sma_sb_ip
  port: !secret sma_sb_port
#http:
#conversation:
#history:
#recorder:
#logbook:
sensor:
  - platform: modbus
    scan_interval: 15
    registers:
      - name: Modbus SB Daily Yield
        unit_of_measurement: kWh
        slave: 3
        register: 30517
        count: 4
        data_type: int
        scale: 0.001
        precision: 3
      - name: Modbus SB Total Yield
        unit_of_measurement: kWh
        slave: 3
        register: 30513
        count: 4
        data_type: int
        scale: 0.001
        precision: 3
      - name: Modbus SB PV Power
        unit_of_measurement: W
        slave: 3
        register: 30775
        count: 2
        data_type: uint
      - name: Modbus SB Grid Power
        unit_of_measurement: W
        slave: 3
        register: 30865
        count: 2
        data_type: uint
      - name: Modbus SB Grid Feed
        unit_of_measurement: W
        slave: 3
        register: 30867
        count: 2
        data_type: uint
  - platform: template
    sensors:
      modbus_sb_pv_production:
        friendly_name: 'PV Output'
        value_template: >-
            {% if states('sensor.modbus_sb_pv_power')|float < 10000 %}
              {{ states('sensor.modbus_sb_pv_power') }}
            {% else %}
              0
            {% endif %}
        unit_of_measurement: "W"
      modbus_sb_total_power:
        friendly_name: 'Total consumption'
        unit_of_measurement: "W"
        value_template: '{{ (((states.sensor.modbus_sb_grid_power.state | float) +
          (states.sensor.modbus_sb_pv_production.state | float))) }}'
automation:
  - id: mqtt_publish_sb
    alias: "MQTT Publish Sunny Boy"
    trigger:
    - platform: state
      entity_id: "sensor.modbus_sb_grid_power"
    action:
    - service: mqtt.publish
      data_template:
        topic: "sma/sb"
        payload: '{"grid_feed":{{ states("sensor.modbus_sb_grid_feed") }}, "grid_consumption":{{ states("sensor.modbus_sb_grid_power") }}, "production":{{ states("sensor.modbus_sb_pv_production") }}, "daily_yield":{{ states("sensor.modbus_sb_daily_yield") }}, "total_yield":{{ states("sensor.modbus_sb_total_yield") }}}'

Here is the MQTT message:

{"grid_feed":0, "grid_consumption":1819, "production":0, "daily_yield":15.884, "total_yield":248.245}

Here are the MQTT sensors in my normal HA instance:

- platform: mqtt
  name: "MQTT SB Daily Yield"
  state_topic: "sma/sb"
  value_template: '{{ value_json["daily_yield"] }}'
  force_update: true
  retain: true
  unit_of_measurement: "kWh"
- platform: mqtt
  name: "MQTT SB Total Yield"
  state_topic: "sma/sb"
  value_template: '{{ value_json["total_yield"] }}'
  force_update: true
  retain: true
  unit_of_measurement: "kWh"
- platform: mqtt
  name: "MQTT SB Production"
  state_topic: "sma/sb"
  value_template: '{{ value_json["production"] }}'
  force_update: true
  retain: true
  unit_of_measurement: "W"
- platform: mqtt
  name: "MQTT SB Grid Feed"
  state_topic: "sma/sb"
  value_template: '{{ value_json["grid_feed"] }}'
  force_update: true
  retain: true
  unit_of_measurement: "W"
- platform: mqtt
  name: "MQTT SB Grid Consumption"
  state_topic: "sma/sb"
  value_template: '{{ value_json["grid_consumption"] }}'
  force_update: true
  retain: true
  unit_of_measurement: "W"

This works really well. The Docker container uses 32MB ram and does not use much processing. This may help someone else who is trying to do the same. Of course, this would not work to change the state of a coil or write data to ModBus, but it’s a working solution to read ModBus sensors on multiple hubs.

1 Like

Hello, I am interested to this feature, because I have more modbus TCP devices and in hass I can get data only from one each time.

I think I figured it out!

Copy /srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/modbus.py
to “your_config”/custom_components
rename it modbus1.py
edit the file by changing DOMAIN = 'modbus1'

image

Now if you want a Modbus sensor

Copy /srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/sensor/modbus.py
to “your_config”/custom_components/sensor
rename it modbus1.py
edit the file by changing import homeassistant.components.modbus as modbus to
import custom_components.modbus1 as modbus
and then DEPENDENCIES = ['modbus1']

image

Note* I am running in a pvenv so I had to make homeassistant the owner of all the directories and files.

  • custom_components, modbus1.py, sensor, modbus1.py

configuration.yaml…

modbus:
  type: tcp
  host: 127.0.0.1
  port: 4000


modbus1:
  type: rtuovertcp
  host: 192.168.254.196
  port: 4598

sensor:
  - platform: modbus
    registers:
      - name: S1
        unit_of_measurement: 'mA'
        slave: 2
        register: 309
        register_type: holding
        count: 1
        precision: 2
        scale: 0.01
        data_type: custom
        structure: ">h"

  - platform: modbus1
    scan_interval: 60
    registers:
      - name: S2
        unit_of_measurement: 'feet'
        slave: 1
        register: 1
        register_type: holding
        count: 2
        scale: 3.281
        precision: 2
        data_type: float

restart HA Done!

2 Likes

Will have to try that one asap. So much cleaner than the solution I had to come up with! Cheers for sharing…

1 Like

Works for me.
Thank you for sharing your easy workaround.

1 Like

@cgrueter @monkey-house

Just testing 1 slave 1 register per connection but so far so good. No real CPU or mem increase.

Let me know how things go for you guys…

I am really counting on multiple connections. I will try to look into full integration.

1 Like

Will hopefully have a look tonight!
@PtP: have you made a pull request on the Home Assistant GitHub. For guys who’ve got experience writing components for Home Assistant, it’s probably easy to come up with a solution that alter the .py file based on the presence of a modus1 entity in the configuration.yaml. Maybe this can become an integral part of Home Assistant by v .73 or .74?

I have not yet but I plan on it!

That’d be great.
Could you also please publish the fully revised code for the modbus1.py custom_component + custom_component/sensor here for ease of upgrading?
It’s a pain to access files for docker containers even locally, let alone remotely… Sure it would make it easier for many users: just copy the modbus1.py files to their respective folders, upgrade configuration.yaml and you’re good to go!

OK, just got to test this as well. It works great. I can finally delete my second instance of Hass and run all in a single container. Thanks @PtP.

Here is the corrected custom_component modbus1.py:

"""
Support for Modbus.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/modbus/
"""
import logging
import threading

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
    CONF_HOST, CONF_METHOD, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, ATTR_STATE)

DOMAIN = 'modbus1'

REQUIREMENTS = ['pymodbus==1.3.1']

# Type of network
CONF_BAUDRATE = 'baudrate'
CONF_BYTESIZE = 'bytesize'
CONF_STOPBITS = 'stopbits'
CONF_PARITY = 'parity'

SERIAL_SCHEMA = {
    vol.Required(CONF_BAUDRATE): cv.positive_int,
    vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
    vol.Required(CONF_METHOD): vol.Any('rtu', 'ascii'),
    vol.Required(CONF_PORT): cv.string,
    vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'),
    vol.Required(CONF_STOPBITS): vol.Any(1, 2),
    vol.Required(CONF_TYPE): 'serial',
    vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}

ETHERNET_SCHEMA = {
    vol.Required(CONF_HOST): cv.string,
    vol.Required(CONF_PORT): cv.positive_int,
    vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'),
    vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}


CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)
}, extra=vol.ALLOW_EXTRA)


_LOGGER = logging.getLogger(__name__)

SERVICE_WRITE_REGISTER = 'write_register'
SERVICE_WRITE_COIL = 'write_coil'

ATTR_ADDRESS = 'address'
ATTR_UNIT = 'unit'
ATTR_VALUE = 'value'

SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({
    vol.Required(ATTR_UNIT): cv.positive_int,
    vol.Required(ATTR_ADDRESS): cv.positive_int,
    vol.Required(ATTR_VALUE): vol.All(cv.ensure_list, [cv.positive_int])
})

SERVICE_WRITE_COIL_SCHEMA = vol.Schema({
    vol.Required(ATTR_UNIT): cv.positive_int,
    vol.Required(ATTR_ADDRESS): cv.positive_int,
    vol.Required(ATTR_STATE): cv.boolean
})

HUB = None


def setup(hass, config):
    """Set up Modbus component."""
    # Modbus connection type
    client_type = config[DOMAIN][CONF_TYPE]

    # Connect to Modbus network
    # pylint: disable=import-error

    if client_type == 'serial':
        from pymodbus.client.sync import ModbusSerialClient as ModbusClient
        client = ModbusClient(method=config[DOMAIN][CONF_METHOD],
                              port=config[DOMAIN][CONF_PORT],
                              baudrate=config[DOMAIN][CONF_BAUDRATE],
                              stopbits=config[DOMAIN][CONF_STOPBITS],
                              bytesize=config[DOMAIN][CONF_BYTESIZE],
                              parity=config[DOMAIN][CONF_PARITY],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'rtuovertcp':
        from pymodbus.client.sync import ModbusTcpClient as ModbusClient
        from pymodbus.transaction import ModbusRtuFramer as ModbusFramer
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              framer=ModbusFramer,
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'tcp':
        from pymodbus.client.sync import ModbusTcpClient as ModbusClient
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'udp':
        from pymodbus.client.sync import ModbusUdpClient as ModbusClient
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    else:
        return False

    global HUB
    HUB = ModbusHub(client)

    def stop_modbus(event):
        """Stop Modbus service."""
        HUB.close()

    def start_modbus(event):
        """Start Modbus service."""
        HUB.connect()
        hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)

        # Register services for modbus
        hass.services.register(
            DOMAIN, SERVICE_WRITE_REGISTER, write_register,
            schema=SERVICE_WRITE_REGISTER_SCHEMA)
        hass.services.register(
            DOMAIN, SERVICE_WRITE_COIL, write_coil,
            schema=SERVICE_WRITE_COIL_SCHEMA)

    def write_register(service):
        """Write modbus registers."""
        unit = int(float(service.data.get(ATTR_UNIT)))
        address = int(float(service.data.get(ATTR_ADDRESS)))
        value = service.data.get(ATTR_VALUE)
        if isinstance(value, list):
            HUB.write_registers(
                unit,
                address,
                [int(float(i)) for i in value])
        else:
            HUB.write_register(
                unit,
                address,
                int(float(value)))

    def write_coil(service):
        """Write modbus coil."""
        unit = service.data.get(ATTR_UNIT)
        address = service.data.get(ATTR_ADDRESS)
        state = service.data.get(ATTR_STATE)
        HUB.write_coil(unit, address, state)

    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)

    return True


class ModbusHub(object):
    """Thread safe wrapper class for pymodbus."""

    def __init__(self, modbus_client):
        """Initialize the modbus hub."""
        self._client = modbus_client
        self._lock = threading.Lock()

    def close(self):
        """Disconnect client."""
        with self._lock:
            self._client.close()

    def connect(self):
        """Connect client."""
        with self._lock:
            self._client.connect()

    def read_coils(self, unit, address, count):
        """Read coils."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_coils(
                address,
                count,
                **kwargs)

    def read_input_registers(self, unit, address, count):
        """Read input registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_input_registers(
                address,
                count,
                **kwargs)

    def read_holding_registers(self, unit, address, count):
        """Read holding registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_holding_registers(
                address,
                count,
                **kwargs)

    def write_coil(self, unit, address, value):
        """Write coil."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_coil(
                address,
                value,
                **kwargs)

    def write_register(self, unit, address, value):
        """Write register."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_register(
                address,
                value,
                **kwargs)

    def write_registers(self, unit, address, values):
        """Write registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_registers(
                address,
                values,
                **kwargs)

And the custom_component/sensor modbus1.py:

"""
Support for Modbus Register sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.modbus/
"""
import logging
import struct

import voluptuous as vol

import custom_components.modbus1 as modbus
from homeassistant.const import (
    CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE,
    CONF_STRUCTURE)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['modbus1']

CONF_COUNT = 'count'
CONF_REVERSE_ORDER = 'reverse_order'
CONF_PRECISION = 'precision'
CONF_REGISTER = 'register'
CONF_REGISTERS = 'registers'
CONF_SCALE = 'scale'
CONF_DATA_TYPE = 'data_type'
CONF_REGISTER_TYPE = 'register_type'

REGISTER_TYPE_HOLDING = 'holding'
REGISTER_TYPE_INPUT = 'input'

DATA_TYPE_INT = 'int'
DATA_TYPE_UINT = 'uint'
DATA_TYPE_FLOAT = 'float'
DATA_TYPE_CUSTOM = 'custom'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_REGISTERS): [{
        vol.Required(CONF_NAME): cv.string,
        vol.Required(CONF_REGISTER): cv.positive_int,
        vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING):
            vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]),
        vol.Optional(CONF_COUNT, default=1): cv.positive_int,
        vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
        vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
        vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
        vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
        vol.Optional(CONF_SLAVE): cv.positive_int,
        vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT):
            vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT,
                    DATA_TYPE_CUSTOM]),
        vol.Optional(CONF_STRUCTURE): cv.string,
        vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string
    }]
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Modbus sensors."""
    sensors = []
    data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}}
    data_types[DATA_TYPE_UINT] = {1: 'H', 2: 'I', 4: 'Q'}
    data_types[DATA_TYPE_FLOAT] = {1: 'e', 2: 'f', 4: 'd'}

    for register in config.get(CONF_REGISTERS):
        structure = '>i'
        if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
            try:
                structure = '>{}'.format(data_types[
                    register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)])
            except KeyError:
                _LOGGER.error("Unable to detect data type for %s sensor, "
                              "try a custom type.", register.get(CONF_NAME))
                continue
        else:
            structure = register.get(CONF_STRUCTURE)

        try:
            size = struct.calcsize(structure)
        except struct.error as err:
            _LOGGER.error(
                "Error in sensor %s structure: %s",
                register.get(CONF_NAME), err)
            continue

        if register.get(CONF_COUNT) * 2 != size:
            _LOGGER.error(
                "Structure size (%d bytes) mismatch registers count "
                "(%d words)", size, register.get(CONF_COUNT))
            continue

        sensors.append(ModbusRegisterSensor(
            register.get(CONF_NAME),
            register.get(CONF_SLAVE),
            register.get(CONF_REGISTER),
            register.get(CONF_REGISTER_TYPE),
            register.get(CONF_UNIT_OF_MEASUREMENT),
            register.get(CONF_COUNT),
            register.get(CONF_REVERSE_ORDER),
            register.get(CONF_SCALE),
            register.get(CONF_OFFSET),
            structure,
            register.get(CONF_PRECISION)))

    if not sensors:
        return False
    add_devices(sensors)


class ModbusRegisterSensor(Entity):
    """Modbus register sensor."""

    def __init__(self, name, slave, register, register_type,
                 unit_of_measurement, count, reverse_order, scale, offset,
                 structure, precision):
        """Initialize the modbus register sensor."""
        self._name = name
        self._slave = int(slave) if slave else None
        self._register = int(register)
        self._register_type = register_type
        self._unit_of_measurement = unit_of_measurement
        self._count = int(count)
        self._reverse_order = reverse_order
        self._scale = scale
        self._offset = offset
        self._precision = precision
        self._structure = structure
        self._value = None

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._value

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return self._unit_of_measurement

    def update(self):
        """Update the state of the sensor."""
        if self._register_type == REGISTER_TYPE_INPUT:
            result = modbus.HUB.read_input_registers(
                self._slave,
                self._register,
                self._count)
        else:
            result = modbus.HUB.read_holding_registers(
                self._slave,
                self._register,
                self._count)
        val = 0

        try:
            registers = result.registers
            if self._reverse_order:
                registers.reverse()
        except AttributeError:
            _LOGGER.error("No response from modbus slave %s, register %s",
                          self._slave, self._register)
            return
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in registers]
        )
        val = struct.unpack(self._structure, byte_string)[0]
        self._value = format(
            self._scale * val + self._offset, '.{}f'.format(self._precision))

Let us know when you post on GitHub so we can support the fixed implementation of multiple modbus or to update the Home Assistant documentation, if we can only add a modbus1.py!

Hi, I just stumbled on this thread when I was trying to implement my second modbus hub. I have already configured modbus tcp but now I was planning to add another one, for modbus rtu. I am running hassio so don’t think I have access to those modbus.py files that you refer to… Does anyone know if it is possible to add both modbus tcp and rtu on hassio…? Thanks in advance.

There is no reason why you could not do this on hass.io. You just need to find out the paths to the custom_components folder (should be in your config folder)…

1 Like

Yes, you are right, it works perfectly now. There was no “custom_components” folder but I just created one inside of the config folder. And then created a “sensor” folder inside that.
Then just copy the 2 modbus1.py files inside those two folders and now it works fine. Thanks everyone for this.

1 Like

Yup, looks like this can be applied to any component. I’m looking at implementing this for Octoprint…

I have 3 Modbus Clients now. Working good! I made a few more code changes. Seems like it helped reduce memory use.

Hi

I’ve been looking for this for ages.

Tried it out and it works. Yay.

Amazing work.

Thank you. Thank you. Thank you.