Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Multiple modbus hubs #19726

Merged
merged 18 commits into from
Feb 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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])