yes, it would be nice. I have several modbus tcp and encapsulated rtu devices.
Is there support for encapsulated rtu?
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.
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.
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'
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']
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!
Will have to try that one asap. So much cleaner than the solution I had to come up with! Cheers for sharing…
Works for me.
Thank you for sharing your easy workaround.
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.
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)…
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.
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.