Skip to content

Commit

Permalink
Support for Multiple modbus hubs (#19726)
Browse files Browse the repository at this point in the history
* modbus: support multiple modbus hub

* update data after entities added

* pass hub object to each entity. and save hub to hass.data but not in module level

* add hub_client setup log

* don't update when adding device, because hub_client is not ready right now

* support restore last state

* remove useless func

* compatible with python35

* removed unrelated style changes

* Update flexit for multi-device modbus

* change how hubs are referenced in the configuration

* Also update climate/modbus.py

* Remove unwanted whitescapce

* Defined common constants centrally

* Update DOMAIN in climate and switch components

* Removed unnecessary vol.schema

* Make hub name optional

* Add name property to ModbusHub
  • Loading branch information
benvm authored and cgarwood committed Feb 11, 2019
1 parent 49ecca9 commit 861d58f
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 92 deletions.
12 changes: 8 additions & 4 deletions homeassistant/components/climate/flexit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE)
from homeassistant.components import modbus
from homeassistant.components.modbus import (
CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN)
import homeassistant.helpers.config_validation as cv

REQUIREMENTS = ['pyflexit==0.3']
DEPENDENCIES = ['modbus']

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)),
vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string
})
Expand All @@ -40,15 +42,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Flexit Platform."""
modbus_slave = config.get(CONF_SLAVE, None)
name = config.get(CONF_NAME, None)
add_entities([Flexit(modbus_slave, name)], True)
hub = hass.data[MODBUS_DOMAIN][config.get(CONF_HUB)]
add_entities([Flexit(hub, modbus_slave, name)], True)


class Flexit(ClimateDevice):
"""Representation of a Flexit AC unit."""

def __init__(self, modbus_slave, name):
def __init__(self, hub, modbus_slave, name):
"""Initialize the unit."""
from pyflexit import pyflexit
self._hub = hub
self._name = name
self._slave = modbus_slave
self._target_temperature = None
Expand All @@ -64,7 +68,7 @@ def __init__(self, modbus_slave, name):
self._heating = None
self._cooling = None
self._alarm = False
self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave)
self.unit = pyflexit.pyflexit(hub, modbus_slave)

@property
def supported_features(self):
Expand Down
120 changes: 71 additions & 49 deletions homeassistant/components/modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,27 @@
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)
CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TYPE, CONF_TIMEOUT,
ATTR_STATE)

DOMAIN = 'modbus'

REQUIREMENTS = ['pymodbus==1.5.2']

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

SERIAL_SCHEMA = {
DEFAULT_HUB = 'default'

BASE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string
})

SERIAL_SCHEMA = BASE_SCHEMA.extend({
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'),
Expand All @@ -33,92 +41,98 @@
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
vol.Required(CONF_TYPE): 'serial',
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}
})

ETHERNET_SCHEMA = {
ETHERNET_SCHEMA = BASE_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
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)

DOMAIN: vol.All(cv.ensure_list, [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_HUB = 'hub'
ATTR_UNIT = 'unit'
ATTR_VALUE = 'value'

SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({
vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
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.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string,
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
def setup_client(client_config):
"""Set up pymodbus client."""
client_type = client_config[CONF_TYPE]

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':
return ModbusClient(method=client_config[CONF_METHOD],
port=client_config[CONF_PORT],
baudrate=client_config[CONF_BAUDRATE],
stopbits=client_config[CONF_STOPBITS],
bytesize=client_config[CONF_BYTESIZE],
parity=client_config[CONF_PARITY],
timeout=client_config[CONF_TIMEOUT])
if 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.transaction import ModbusRtuFramer
return ModbusClient(host=client_config[CONF_HOST],
port=client_config[CONF_PORT],
framer=ModbusRtuFramer,
timeout=client_config[CONF_TIMEOUT])
if 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':
return ModbusClient(host=client_config[CONF_HOST],
port=client_config[CONF_PORT],
timeout=client_config[CONF_TIMEOUT])
if 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
return ModbusClient(host=client_config[CONF_HOST],
port=client_config[CONF_PORT],
timeout=client_config[CONF_TIMEOUT])
assert False

global HUB
HUB = ModbusHub(client)

def setup(hass, config):
"""Set up Modbus component."""
# Modbus connection type
hass.data[DOMAIN] = hub_collect = {}

for client_config in config[DOMAIN]:
client = setup_client(client_config)
name = client_config[CONF_NAME]
hub_collect[name] = ModbusHub(client, name)
_LOGGER.debug('Setting up hub: %s', client_config)

def stop_modbus(event):
"""Stop Modbus service."""
HUB.close()
for client in hub_collect.values():
client.close()

def start_modbus(event):
"""Start Modbus service."""
HUB.connect()
for client in hub_collect.values():
client.connect()

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)

# Register services for modbus
Expand All @@ -134,13 +148,14 @@ def write_register(service):
unit = int(float(service.data.get(ATTR_UNIT)))
address = int(float(service.data.get(ATTR_ADDRESS)))
value = service.data.get(ATTR_VALUE)
client_name = service.data.get(ATTR_HUB)
if isinstance(value, list):
HUB.write_registers(
hub_collect[client_name].write_registers(
unit,
address,
[int(float(i)) for i in value])
else:
HUB.write_register(
hub_collect[client_name].write_register(
unit,
address,
int(float(value)))
Expand All @@ -150,7 +165,8 @@ def write_coil(service):
unit = service.data.get(ATTR_UNIT)
address = service.data.get(ATTR_ADDRESS)
state = service.data.get(ATTR_STATE)
HUB.write_coil(unit, address, state)
client_name = service.data.get(ATTR_HUB)
hub_collect[client_name].write_coil(unit, address, state)

hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)

Expand All @@ -160,10 +176,16 @@ def write_coil(service):
class ModbusHub:
"""Thread safe wrapper class for pymodbus."""

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

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

def close(self):
"""Disconnect client."""
Expand Down
17 changes: 10 additions & 7 deletions homeassistant/components/modbus/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import logging
import voluptuous as vol

from homeassistant.components import modbus
from homeassistant.components.modbus import (
CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN)
from homeassistant.const import CONF_NAME, CONF_SLAVE
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers import config_validation as cv
Expand All @@ -21,6 +22,7 @@

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COILS): [{
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Required(CONF_COIL): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_SLAVE): cv.positive_int
Expand All @@ -32,7 +34,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Modbus binary sensors."""
sensors = []
for coil in config.get(CONF_COILS):
hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)]
sensors.append(ModbusCoilSensor(
hub,
coil.get(CONF_NAME),
coil.get(CONF_SLAVE),
coil.get(CONF_COIL)))
Expand All @@ -42,8 +46,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class ModbusCoilSensor(BinarySensorDevice):
"""Modbus coil sensor."""

def __init__(self, name, slave, coil):
def __init__(self, hub, name, slave, coil):
"""Initialize the modbus coil sensor."""
self._hub = hub
self._name = name
self._slave = int(slave) if slave else None
self._coil = int(coil)
Expand All @@ -61,11 +66,9 @@ def is_on(self):

def update(self):
"""Update the state of the sensor."""
result = modbus.HUB.read_coils(self._slave, self._coil, 1)
result = self._hub.read_coils(self._slave, self._coil, 1)
try:
self._value = result.bits[0]
except AttributeError:
_LOGGER.error(
'No response from modbus slave %s coil %s',
self._slave,
self._coil)
_LOGGER.error('No response from hub %s, slave %s, coil %s',
self._hub.name, self._slave, self._coil)
18 changes: 11 additions & 7 deletions homeassistant/components/modbus/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE)
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)

from homeassistant.components import modbus
from homeassistant.components.modbus import (
CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN)
import homeassistant.helpers.config_validation as cv

DEPENDENCIES = ['modbus']
Expand All @@ -35,6 +35,7 @@
DATA_TYPE_FLOAT = 'float'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SLAVE): cv.positive_int,
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
Expand All @@ -59,18 +60,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data_type = config.get(CONF_DATA_TYPE)
count = config.get(CONF_COUNT)
precision = config.get(CONF_PRECISION)
hub_name = config.get(CONF_HUB)
hub = hass.data[MODBUS_DOMAIN][hub_name]

add_entities([ModbusThermostat(name, modbus_slave,
add_entities([ModbusThermostat(hub, name, modbus_slave,
target_temp_register, current_temp_register,
data_type, count, precision)], True)


class ModbusThermostat(ClimateDevice):
"""Representation of a Modbus Thermostat."""

def __init__(self, name, modbus_slave, target_temp_register,
def __init__(self, hub, name, modbus_slave, target_temp_register,
current_temp_register, data_type, count, precision):
"""Initialize the unit."""
self._hub = hub
self._name = name
self._slave = modbus_slave
self._target_temperature_register = target_temp_register
Expand Down Expand Up @@ -133,8 +137,8 @@ def set_temperature(self, **kwargs):
def read_register(self, register):
"""Read holding register using the modbus hub slave."""
try:
result = modbus.HUB.read_holding_registers(self._slave, register,
self._count)
result = self._hub.read_holding_registers(self._slave, register,
self._count)
except AttributeError as ex:
_LOGGER.error(ex)
byte_string = b''.join(
Expand All @@ -145,4 +149,4 @@ def read_register(self, register):

def write_register(self, register, value):
"""Write register using the modbus hub slave."""
modbus.HUB.write_registers(self._slave, register, [value, 0])
self._hub.write_registers(self._slave, register, [value, 0])

0 comments on commit 861d58f

Please sign in to comment.