Rollback modbus to version 0.107.7 keep new functionality (#34287)

* Rollback modbus to version 0.107.7

Update manifest to not use async.

Rollback entities to sync version.

Keep newer modifications apart from async.

Rollback __init__ to sync version but keep the new functionality.

add async sub directory

Adding the current (not working) version in a sub directory,
to allow easy sharing with a few alfa testers.

The async version are to be updated to use the serial/tcp already
available instead of the flaky pymodbus version. pymodbus is still
needed to encode/decode the messagess.

Update test cases to reflect sync implementation, but
keep the new functionality like e.g. conftest.py.

* do not publish async version

The async version will be made available in a forked repo, until
it is ready to replace the production code.
pull/34337/head
jan iversen 2020-04-17 09:55:57 +02:00 committed by GitHub
parent 9aee91b98c
commit 8277ebcbe1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 230 deletions

View File

@ -1,23 +1,9 @@
"""Support for Modbus."""
import asyncio
import logging
import threading
from async_timeout import timeout
from pymodbus.client.asynchronous.asyncio import (
AsyncioModbusSerialClient,
ModbusClientProtocol,
init_tcp_client,
init_udp_client,
)
from pymodbus.exceptions import ModbusException
from pymodbus.factory import ClientDecoder
from pymodbus.pdu import ExceptionResponse
from pymodbus.transaction import (
ModbusAsciiFramer,
ModbusBinaryFramer,
ModbusRtuFramer,
ModbusSocketFramer,
)
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient
from pymodbus.transaction import ModbusRtuFramer
import voluptuous as vol
from homeassistant.const import (
@ -50,6 +36,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
SERIAL_SCHEMA = BASE_SCHEMA.extend(
@ -101,57 +88,55 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema(
)
async def async_setup(hass, config):
def setup(hass, config):
"""Set up Modbus component."""
hass.data[DOMAIN] = hub_collect = {}
for client_config in config[DOMAIN]:
hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop)
hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config)
def stop_modbus(event):
"""Stop Modbus service."""
for client in hub_collect.values():
del client
client.close()
async def write_register(service):
def write_register(service):
"""Write Modbus registers."""
unit = int(float(service.data[ATTR_UNIT]))
address = int(float(service.data[ATTR_ADDRESS]))
value = service.data[ATTR_VALUE]
client_name = service.data[ATTR_HUB]
if isinstance(value, list):
await hub_collect[client_name].write_registers(
hub_collect[client_name].write_registers(
unit, address, [int(float(i)) for i in value]
)
else:
await hub_collect[client_name].write_register(
unit, address, int(float(value))
)
hub_collect[client_name].write_register(unit, address, int(float(value)))
async def write_coil(service):
def write_coil(service):
"""Write Modbus coil."""
unit = service.data[ATTR_UNIT]
address = service.data[ATTR_ADDRESS]
state = service.data[ATTR_STATE]
client_name = service.data[ATTR_HUB]
await hub_collect[client_name].write_coil(unit, address, state)
hub_collect[client_name].write_coil(unit, address, state)
# do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now
for client in hub_collect.values():
await client.setup(hass)
client.setup()
# register function to gracefully stop modbus
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
# Register services for modbus
hass.services.async_register(
hass.services.register(
DOMAIN,
SERVICE_WRITE_REGISTER,
write_register,
schema=SERVICE_WRITE_REGISTER_SCHEMA,
)
hass.services.async_register(
DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA,
hass.services.register(
DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA
)
return True
@ -159,13 +144,12 @@ async def async_setup(hass, config):
class ModbusHub:
"""Thread safe wrapper class for pymodbus."""
def __init__(self, client_config, main_loop):
def __init__(self, client_config):
"""Initialize the Modbus hub."""
# generic configuration
self._loop = main_loop
self._client = None
self._lock = asyncio.Lock()
self._lock = threading.Lock()
self._config_name = client_config[CONF_NAME]
self._config_type = client_config[CONF_TYPE]
self._config_port = client_config[CONF_PORT]
@ -183,144 +167,101 @@ class ModbusHub:
# network configuration
self._config_host = client_config[CONF_HOST]
self._config_delay = client_config[CONF_DELAY]
if self._config_delay > 0:
_LOGGER.warning(
"Parameter delay is accepted but not used in this version"
)
@property
def name(self):
"""Return the name of this hub."""
return self._config_name
async def _connect_delay(self):
if self._config_delay > 0:
await asyncio.sleep(self._config_delay)
self._config_delay = 0
@staticmethod
def _framer(method):
if method == "ascii":
framer = ModbusAsciiFramer(ClientDecoder())
elif method == "rtu":
framer = ModbusRtuFramer(ClientDecoder())
elif method == "binary":
framer = ModbusBinaryFramer(ClientDecoder())
elif method == "socket":
framer = ModbusSocketFramer(ClientDecoder())
else:
framer = None
return framer
async def setup(self, hass):
def setup(self):
"""Set up pymodbus client."""
if self._config_type == "serial":
# reconnect ??
framer = self._framer(self._config_method)
# just a class creation no IO or other slow items
self._client = AsyncioModbusSerialClient(
self._config_port,
protocol_class=ModbusClientProtocol,
framer=framer,
loop=self._loop,
self._client = ModbusSerialClient(
method=self._config_method,
port=self._config_port,
baudrate=self._config_baudrate,
stopbits=self._config_stopbits,
bytesize=self._config_bytesize,
parity=self._config_parity,
stopbits=self._config_stopbits,
timeout=self._config_timeout,
)
await self._client.connect()
elif self._config_type == "rtuovertcp":
# framer ModbusRtuFramer ??
# timeout ??
self._client = await init_tcp_client(
None, self._loop, self._config_host, self._config_port
self._client = ModbusTcpClient(
host=self._config_host,
port=self._config_port,
framer=ModbusRtuFramer,
timeout=self._config_timeout,
)
elif self._config_type == "tcp":
# framer ??
# timeout ??
self._client = await init_tcp_client(
None, self._loop, self._config_host, self._config_port
self._client = ModbusTcpClient(
host=self._config_host,
port=self._config_port,
timeout=self._config_timeout,
)
elif self._config_type == "udp":
# framer ??
# timeout ??
self._client = await init_udp_client(
None, self._loop, self._config_host, self._config_port
self._client = ModbusUdpClient(
host=self._config_host,
port=self._config_port,
timeout=self._config_timeout,
)
else:
assert False
async def _read(self, unit, address, count, func):
"""Read generic with error handling."""
await self._connect_delay()
async with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
async with timeout(self._config_timeout):
result = await func(address, count, **kwargs)
except asyncio.TimeoutError:
result = None
# Connect device
self.connect()
if isinstance(result, (ModbusException, ExceptionResponse)):
_LOGGER.error("Hub %s Exception (%s)", self._config_name, result)
return result
def close(self):
"""Disconnect client."""
with self._lock:
self._client.close()
async def _write(self, unit, address, value, func):
"""Read generic with error handling."""
await self._connect_delay()
async with self._lock:
kwargs = {"unit": unit} if unit else {}
try:
async with timeout(self._config_timeout):
func(address, value, **kwargs)
except asyncio.TimeoutError:
return
def connect(self):
"""Connect client."""
with self._lock:
self._client.connect()
async def read_coils(self, unit, address, count):
def read_coils(self, unit, address, count):
"""Read coils."""
if self._client.protocol is None:
return None
return await self._read(unit, address, count, self._client.protocol.read_coils)
with self._lock:
kwargs = {"unit": unit} if unit else {}
return self._client.read_coils(address, count, **kwargs)
async def read_discrete_inputs(self, unit, address, count):
def read_discrete_inputs(self, unit, address, count):
"""Read discrete inputs."""
if self._client.protocol is None:
return None
return await self._read(
unit, address, count, self._client.protocol.read_discrete_inputs
)
with self._lock:
kwargs = {"unit": unit} if unit else {}
return self._client.read_discrete_inputs(address, count, **kwargs)
async def read_input_registers(self, unit, address, count):
def read_input_registers(self, unit, address, count):
"""Read input registers."""
if self._client.protocol is None:
return None
return await self._read(
unit, address, count, self._client.protocol.read_input_registers
)
with self._lock:
kwargs = {"unit": unit} if unit else {}
return self._client.read_input_registers(address, count, **kwargs)
async def read_holding_registers(self, unit, address, count):
def read_holding_registers(self, unit, address, count):
"""Read holding registers."""
if self._client.protocol is None:
return None
return await self._read(
unit, address, count, self._client.protocol.read_holding_registers
)
with self._lock:
kwargs = {"unit": unit} if unit else {}
return self._client.read_holding_registers(address, count, **kwargs)
async def write_coil(self, unit, address, value):
def write_coil(self, unit, address, value):
"""Write coil."""
if self._client.protocol is None:
return None
return await self._write(unit, address, value, self._client.protocol.write_coil)
with self._lock:
kwargs = {"unit": unit} if unit else {}
self._client.write_coil(address, value, **kwargs)
async def write_register(self, unit, address, value):
def write_register(self, unit, address, value):
"""Write register."""
if self._client.protocol is None:
return None
return await self._write(
unit, address, value, self._client.protocol.write_register
)
with self._lock:
kwargs = {"unit": unit} if unit else {}
self._client.write_register(address, value, **kwargs)
async def write_registers(self, unit, address, values):
def write_registers(self, unit, address, values):
"""Write registers."""
if self._client.protocol is None:
return None
return await self._write(
unit, address, values, self._client.protocol.write_registers
)
with self._lock:
kwargs = {"unit": unit} if unit else {}
self._client.write_registers(address, values, **kwargs)

View File

@ -2,7 +2,7 @@
import logging
from typing import Optional
from pymodbus.exceptions import ModbusException
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
@ -54,7 +54,7 @@ PLATFORM_SCHEMA = vol.All(
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Modbus binary sensors."""
sensors = []
for entry in config[CONF_INPUTS]:
@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
)
async_add_entities(sensors)
add_entities(sensors)
class ModbusBinarySensor(BinarySensorDevice):
@ -107,15 +107,17 @@ class ModbusBinarySensor(BinarySensorDevice):
"""Return True if entity is available."""
return self._available
async def async_update(self):
def update(self):
"""Update the state of the sensor."""
try:
if self._input_type == CALL_TYPE_COIL:
result = await self._hub.read_coils(self._slave, self._address, 1)
result = self._hub.read_coils(self._slave, self._address, 1)
else:
result = await self._hub.read_discrete_inputs(self._slave, self._address, 1)
if result is None:
result = self._hub.read_discrete_inputs(self._slave, self._address, 1)
except ConnectionException:
self._available = False
return
if isinstance(result, (ModbusException, ExceptionResponse)):
self._available = False
return

View File

@ -3,7 +3,7 @@ import logging
import struct
from typing import Optional
from pymodbus.exceptions import ModbusException
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
@ -72,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Modbus Thermostat Platform."""
name = config[CONF_NAME]
modbus_slave = config[CONF_SLAVE]
@ -91,7 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
hub_name = config[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
async_add_entities(
add_entities(
[
ModbusThermostat(
hub,
@ -170,12 +170,12 @@ class ModbusThermostat(ClimateDevice):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
async def async_update(self):
def update(self):
"""Update Target & Current Temperature."""
self._target_temperature = await self._read_register(
self._target_temperature = self._read_register(
CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register
)
self._current_temperature = await self._read_register(
self._current_temperature = self._read_register(
self._current_temperature_register_type, self._current_temperature_register
)
@ -224,7 +224,7 @@ class ModbusThermostat(ClimateDevice):
"""Return the supported step of target temperature."""
return self._temp_step
async def set_temperature(self, **kwargs):
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temperature = int(
(kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale
@ -233,26 +233,28 @@ class ModbusThermostat(ClimateDevice):
return
byte_string = struct.pack(self._structure, target_temperature)
register_value = struct.unpack(">h", byte_string[0:2])[0]
await self._write_register(self._target_temperature_register, register_value)
self._write_register(self._target_temperature_register, register_value)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
async def _read_register(self, register_type, register) -> Optional[float]:
def _read_register(self, register_type, register) -> Optional[float]:
"""Read register using the Modbus hub slave."""
try:
if register_type == CALL_TYPE_REGISTER_INPUT:
result = await self._hub.read_input_registers(
result = self._hub.read_input_registers(
self._slave, register, self._count
)
else:
result = await self._hub.read_holding_registers(
result = self._hub.read_holding_registers(
self._slave, register, self._count
)
if result is None:
except ConnectionException:
self._available = False
return
if isinstance(result, (ModbusException, ExceptionResponse)):
self._available = False
return
@ -269,7 +271,12 @@ class ModbusThermostat(ClimateDevice):
return register_value
async def _write_register(self, register, value):
def _write_register(self, register, value):
"""Write holding register using the Modbus hub slave."""
await self._hub.write_registers(self._slave, register, [value, 0])
try:
self._hub.write_registers(self._slave, register, [value, 0])
except ConnectionException:
self._available = False
return
self._available = True

View File

@ -2,7 +2,6 @@
"domain": "modbus",
"name": "Modbus",
"documentation": "https://www.home-assistant.io/integrations/modbus",
"requirements": ["pymodbus==2.3.0",
"pyserial-asyncio==0.4"],
"requirements": ["pymodbus==2.3.0"],
"codeowners": ["@adamchengtkc", "@janiversen"]
}

View File

@ -3,7 +3,7 @@ import logging
import struct
from typing import Any, Optional, Union
from pymodbus.exceptions import ModbusException
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
@ -89,7 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Modbus sensors."""
sensors = []
data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}}
@ -148,7 +148,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
if not sensors:
return False
async_add_entities(sensors)
add_entities(sensors)
class ModbusRegisterSensor(RestoreEntity):
@ -219,19 +219,21 @@ class ModbusRegisterSensor(RestoreEntity):
"""Return True if entity is available."""
return self._available
async def async_update(self):
def update(self):
"""Update the state of the sensor."""
try:
if self._register_type == CALL_TYPE_REGISTER_INPUT:
result = await self._hub.read_input_registers(
result = self._hub.read_input_registers(
self._slave, self._register, self._count
)
else:
result = await self._hub.read_holding_registers(
result = self._hub.read_holding_registers(
self._slave, self._register, self._count
)
if result is None:
except ConnectionException:
self._available = False
return
if isinstance(result, (ModbusException, ExceptionResponse)):
self._available = False
return
@ -246,7 +248,7 @@ class ModbusRegisterSensor(RestoreEntity):
if isinstance(val, int):
self._value = str(val)
if self._precision > 0:
self._value += f".{'0' * self._precision}"
self._value += "." + "0" * self._precision
else:
self._value = f"{val:.{self._precision}f}"

View File

@ -2,7 +2,7 @@
import logging
from typing import Optional
from pymodbus.exceptions import ModbusException
from pymodbus.exceptions import ConnectionException, ModbusException
from pymodbus.pdu import ExceptionResponse
import voluptuous as vol
@ -76,7 +76,7 @@ PLATFORM_SCHEMA = vol.All(
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Read configuration and create Modbus devices."""
switches = []
if CONF_COILS in config:
@ -109,7 +109,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
)
)
async_add_entities(switches)
add_entities(switches)
class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
@ -146,24 +146,26 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
"""Return True if entity is available."""
return self._available
async def turn_on(self, **kwargs):
def turn_on(self, **kwargs):
"""Set switch on."""
await self._write_coil(self._coil, True)
self._write_coil(self._coil, True)
async def turn_off(self, **kwargs):
def turn_off(self, **kwargs):
"""Set switch off."""
await self._write_coil(self._coil, False)
self._write_coil(self._coil, False)
async def async_update(self):
def update(self):
"""Update the state of the switch."""
self._is_on = await self._read_coil(self._coil)
self._is_on = self._read_coil(self._coil)
async def _read_coil(self, coil) -> Optional[bool]:
def _read_coil(self, coil) -> Optional[bool]:
"""Read coil using the Modbus hub slave."""
result = await self._hub.read_coils(self._slave, coil, 1)
if result is None:
try:
result = self._hub.read_coils(self._slave, coil, 1)
except ConnectionException:
self._available = False
return
if isinstance(result, (ModbusException, ExceptionResponse)):
self._available = False
return
@ -173,9 +175,14 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
return value
async def _write_coil(self, coil, value):
def _write_coil(self, coil, value):
"""Write coil using the Modbus hub slave."""
await self._hub.write_coil(self._slave, coil, value)
try:
self._hub.write_coil(self._slave, coil, value)
except ConnectionException:
self._available = False
return
self._available = True
@ -221,21 +228,21 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
self._is_on = None
async def turn_on(self, **kwargs):
def turn_on(self, **kwargs):
"""Set switch on."""
# Only holding register is writable
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
await self._write_register(self._command_on)
self._write_register(self._command_on)
if not self._verify_state:
self._is_on = True
async def turn_off(self, **kwargs):
def turn_off(self, **kwargs):
"""Set switch off."""
# Only holding register is writable
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
await self._write_register(self._command_off)
self._write_register(self._command_off)
if not self._verify_state:
self._is_on = False
@ -244,12 +251,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
"""Return True if entity is available."""
return self._available
async def async_update(self):
def update(self):
"""Update the state of the switch."""
if not self._verify_state:
return
value = await self._read_register()
value = self._read_register()
if value == self._state_on:
self._is_on = True
elif value == self._state_off:
@ -263,18 +270,18 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
value,
)
async def _read_register(self) -> Optional[int]:
def _read_register(self) -> Optional[int]:
try:
if self._register_type == CALL_TYPE_REGISTER_INPUT:
result = await self._hub.read_input_registers(
self._slave, self._register, 1
)
result = self._hub.read_input_registers(self._slave, self._register, 1)
else:
result = await self._hub.read_holding_registers(
result = self._hub.read_holding_registers(
self._slave, self._register, 1
)
if result is None:
except ConnectionException:
self._available = False
return
if isinstance(result, (ModbusException, ExceptionResponse)):
self._available = False
return
@ -284,7 +291,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
return value
async def _write_register(self, value):
def _write_register(self, value):
"""Write holding register using the Modbus hub slave."""
await self._hub.write_register(self._slave, self._register, value)
try:
self._hub.write_register(self._slave, self._register, value)
except ConnectionException:
self._available = False
return
self._available = True

View File

@ -1531,7 +1531,6 @@ pysdcp==1
# homeassistant.components.sensibo
pysensibo==1.0.3
# homeassistant.components.modbus
# homeassistant.components.serial
pyserial-asyncio==0.4

View File

@ -600,10 +600,6 @@ pyps4-2ndscreen==1.0.7
# homeassistant.components.qwikswitch
pyqwikswitch==0.93
# homeassistant.components.modbus
# homeassistant.components.serial
pyserial-asyncio==0.4
# homeassistant.components.signal_messenger
pysignalclirestapi==0.2.4

View File

@ -40,19 +40,11 @@ class ReadResult:
self.registers = register_words
read_result = None
async def run_test(
hass, use_mock_hub, register_config, entity_domain, register_words, expected
):
"""Run test for given config and check that sensor outputs expected result."""
async def simulate_read_registers(unit, address, count):
"""Simulate modbus register read."""
del unit, address, count # not used in simulation, but in real connection
return read_result
# Full sensor configuration
sensor_name = "modbus_test_sensor"
scan_interval = 5
@ -69,9 +61,9 @@ async def run_test(
# Setup inputs for the sensor
read_result = ReadResult(register_words)
if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT:
use_mock_hub.read_input_registers = simulate_read_registers
use_mock_hub.read_input_registers.return_value = read_result
else:
use_mock_hub.read_holding_registers = simulate_read_registers
use_mock_hub.read_holding_registers.return_value = read_result
# Initialize sensor
now = dt_util.utcnow()