mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
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.
This commit is contained in:
parent
9aee91b98c
commit
8277ebcbe1
@ -1,23 +1,9 @@
|
|||||||
"""Support for Modbus."""
|
"""Support for Modbus."""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
from async_timeout import timeout
|
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient
|
||||||
from pymodbus.client.asynchronous.asyncio import (
|
from pymodbus.transaction import ModbusRtuFramer
|
||||||
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,
|
|
||||||
)
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -50,6 +36,7 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
|
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
|
||||||
|
|
||||||
SERIAL_SCHEMA = BASE_SCHEMA.extend(
|
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."""
|
"""Set up Modbus component."""
|
||||||
hass.data[DOMAIN] = hub_collect = {}
|
hass.data[DOMAIN] = hub_collect = {}
|
||||||
|
|
||||||
for client_config in config[DOMAIN]:
|
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):
|
def stop_modbus(event):
|
||||||
"""Stop Modbus service."""
|
"""Stop Modbus service."""
|
||||||
for client in hub_collect.values():
|
for client in hub_collect.values():
|
||||||
del client
|
client.close()
|
||||||
|
|
||||||
async def write_register(service):
|
def write_register(service):
|
||||||
"""Write Modbus registers."""
|
"""Write Modbus registers."""
|
||||||
unit = int(float(service.data[ATTR_UNIT]))
|
unit = int(float(service.data[ATTR_UNIT]))
|
||||||
address = int(float(service.data[ATTR_ADDRESS]))
|
address = int(float(service.data[ATTR_ADDRESS]))
|
||||||
value = service.data[ATTR_VALUE]
|
value = service.data[ATTR_VALUE]
|
||||||
client_name = service.data[ATTR_HUB]
|
client_name = service.data[ATTR_HUB]
|
||||||
if isinstance(value, list):
|
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]
|
unit, address, [int(float(i)) for i in value]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await hub_collect[client_name].write_register(
|
hub_collect[client_name].write_register(unit, address, int(float(value)))
|
||||||
unit, address, int(float(value))
|
|
||||||
)
|
|
||||||
|
|
||||||
async def write_coil(service):
|
def write_coil(service):
|
||||||
"""Write Modbus coil."""
|
"""Write Modbus coil."""
|
||||||
unit = service.data[ATTR_UNIT]
|
unit = service.data[ATTR_UNIT]
|
||||||
address = service.data[ATTR_ADDRESS]
|
address = service.data[ATTR_ADDRESS]
|
||||||
state = service.data[ATTR_STATE]
|
state = service.data[ATTR_STATE]
|
||||||
client_name = service.data[ATTR_HUB]
|
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
|
# do not wait for EVENT_HOMEASSISTANT_START, activate pymodbus now
|
||||||
for client in hub_collect.values():
|
for client in hub_collect.values():
|
||||||
await client.setup(hass)
|
client.setup()
|
||||||
|
|
||||||
# register function to gracefully stop modbus
|
# register function to gracefully stop modbus
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
|
||||||
|
|
||||||
# Register services for modbus
|
# Register services for modbus
|
||||||
hass.services.async_register(
|
hass.services.register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_WRITE_REGISTER,
|
SERVICE_WRITE_REGISTER,
|
||||||
write_register,
|
write_register,
|
||||||
schema=SERVICE_WRITE_REGISTER_SCHEMA,
|
schema=SERVICE_WRITE_REGISTER_SCHEMA,
|
||||||
)
|
)
|
||||||
hass.services.async_register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA,
|
DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -159,13 +144,12 @@ async def async_setup(hass, config):
|
|||||||
class ModbusHub:
|
class ModbusHub:
|
||||||
"""Thread safe wrapper class for pymodbus."""
|
"""Thread safe wrapper class for pymodbus."""
|
||||||
|
|
||||||
def __init__(self, client_config, main_loop):
|
def __init__(self, client_config):
|
||||||
"""Initialize the Modbus hub."""
|
"""Initialize the Modbus hub."""
|
||||||
|
|
||||||
# generic configuration
|
# generic configuration
|
||||||
self._loop = main_loop
|
|
||||||
self._client = None
|
self._client = None
|
||||||
self._lock = asyncio.Lock()
|
self._lock = threading.Lock()
|
||||||
self._config_name = client_config[CONF_NAME]
|
self._config_name = client_config[CONF_NAME]
|
||||||
self._config_type = client_config[CONF_TYPE]
|
self._config_type = client_config[CONF_TYPE]
|
||||||
self._config_port = client_config[CONF_PORT]
|
self._config_port = client_config[CONF_PORT]
|
||||||
@ -183,144 +167,101 @@ class ModbusHub:
|
|||||||
# network configuration
|
# network configuration
|
||||||
self._config_host = client_config[CONF_HOST]
|
self._config_host = client_config[CONF_HOST]
|
||||||
self._config_delay = client_config[CONF_DELAY]
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of this hub."""
|
"""Return the name of this hub."""
|
||||||
return self._config_name
|
return self._config_name
|
||||||
|
|
||||||
async def _connect_delay(self):
|
def setup(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):
|
|
||||||
"""Set up pymodbus client."""
|
"""Set up pymodbus client."""
|
||||||
if self._config_type == "serial":
|
if self._config_type == "serial":
|
||||||
# reconnect ??
|
self._client = ModbusSerialClient(
|
||||||
framer = self._framer(self._config_method)
|
method=self._config_method,
|
||||||
|
port=self._config_port,
|
||||||
# just a class creation no IO or other slow items
|
|
||||||
self._client = AsyncioModbusSerialClient(
|
|
||||||
self._config_port,
|
|
||||||
protocol_class=ModbusClientProtocol,
|
|
||||||
framer=framer,
|
|
||||||
loop=self._loop,
|
|
||||||
baudrate=self._config_baudrate,
|
baudrate=self._config_baudrate,
|
||||||
|
stopbits=self._config_stopbits,
|
||||||
bytesize=self._config_bytesize,
|
bytesize=self._config_bytesize,
|
||||||
parity=self._config_parity,
|
parity=self._config_parity,
|
||||||
stopbits=self._config_stopbits,
|
timeout=self._config_timeout,
|
||||||
)
|
)
|
||||||
await self._client.connect()
|
|
||||||
elif self._config_type == "rtuovertcp":
|
elif self._config_type == "rtuovertcp":
|
||||||
# framer ModbusRtuFramer ??
|
self._client = ModbusTcpClient(
|
||||||
# timeout ??
|
host=self._config_host,
|
||||||
self._client = await init_tcp_client(
|
port=self._config_port,
|
||||||
None, self._loop, self._config_host, self._config_port
|
framer=ModbusRtuFramer,
|
||||||
|
timeout=self._config_timeout,
|
||||||
)
|
)
|
||||||
elif self._config_type == "tcp":
|
elif self._config_type == "tcp":
|
||||||
# framer ??
|
self._client = ModbusTcpClient(
|
||||||
# timeout ??
|
host=self._config_host,
|
||||||
self._client = await init_tcp_client(
|
port=self._config_port,
|
||||||
None, self._loop, self._config_host, self._config_port
|
timeout=self._config_timeout,
|
||||||
)
|
)
|
||||||
elif self._config_type == "udp":
|
elif self._config_type == "udp":
|
||||||
# framer ??
|
self._client = ModbusUdpClient(
|
||||||
# timeout ??
|
host=self._config_host,
|
||||||
self._client = await init_udp_client(
|
port=self._config_port,
|
||||||
None, self._loop, self._config_host, self._config_port
|
timeout=self._config_timeout,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
async def _read(self, unit, address, count, func):
|
# Connect device
|
||||||
"""Read generic with error handling."""
|
self.connect()
|
||||||
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
|
|
||||||
|
|
||||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
def close(self):
|
||||||
_LOGGER.error("Hub %s Exception (%s)", self._config_name, result)
|
"""Disconnect client."""
|
||||||
return result
|
with self._lock:
|
||||||
|
self._client.close()
|
||||||
|
|
||||||
async def _write(self, unit, address, value, func):
|
def connect(self):
|
||||||
"""Read generic with error handling."""
|
"""Connect client."""
|
||||||
await self._connect_delay()
|
with self._lock:
|
||||||
async with self._lock:
|
self._client.connect()
|
||||||
kwargs = {"unit": unit} if unit else {}
|
|
||||||
try:
|
|
||||||
async with timeout(self._config_timeout):
|
|
||||||
func(address, value, **kwargs)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
return
|
|
||||||
|
|
||||||
async def read_coils(self, unit, address, count):
|
def read_coils(self, unit, address, count):
|
||||||
"""Read coils."""
|
"""Read coils."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._read(unit, address, count, self._client.protocol.read_coils)
|
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."""
|
"""Read discrete inputs."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._read(
|
return self._client.read_discrete_inputs(address, count, **kwargs)
|
||||||
unit, address, count, self._client.protocol.read_discrete_inputs
|
|
||||||
)
|
|
||||||
|
|
||||||
async def read_input_registers(self, unit, address, count):
|
def read_input_registers(self, unit, address, count):
|
||||||
"""Read input registers."""
|
"""Read input registers."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._read(
|
return self._client.read_input_registers(address, count, **kwargs)
|
||||||
unit, address, count, self._client.protocol.read_input_registers
|
|
||||||
)
|
|
||||||
|
|
||||||
async def read_holding_registers(self, unit, address, count):
|
def read_holding_registers(self, unit, address, count):
|
||||||
"""Read holding registers."""
|
"""Read holding registers."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._read(
|
return self._client.read_holding_registers(address, count, **kwargs)
|
||||||
unit, address, count, self._client.protocol.read_holding_registers
|
|
||||||
)
|
|
||||||
|
|
||||||
async def write_coil(self, unit, address, value):
|
def write_coil(self, unit, address, value):
|
||||||
"""Write coil."""
|
"""Write coil."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._write(unit, address, value, self._client.protocol.write_coil)
|
self._client.write_coil(address, value, **kwargs)
|
||||||
|
|
||||||
async def write_register(self, unit, address, value):
|
def write_register(self, unit, address, value):
|
||||||
"""Write register."""
|
"""Write register."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._write(
|
self._client.write_register(address, value, **kwargs)
|
||||||
unit, address, value, self._client.protocol.write_register
|
|
||||||
)
|
|
||||||
|
|
||||||
async def write_registers(self, unit, address, values):
|
def write_registers(self, unit, address, values):
|
||||||
"""Write registers."""
|
"""Write registers."""
|
||||||
if self._client.protocol is None:
|
with self._lock:
|
||||||
return None
|
kwargs = {"unit": unit} if unit else {}
|
||||||
return await self._write(
|
self._client.write_registers(address, values, **kwargs)
|
||||||
unit, address, values, self._client.protocol.write_registers
|
|
||||||
)
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pymodbus.exceptions import ModbusException
|
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||||
from pymodbus.pdu import ExceptionResponse
|
from pymodbus.pdu import ExceptionResponse
|
||||||
import voluptuous as vol
|
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."""
|
"""Set up the Modbus binary sensors."""
|
||||||
sensors = []
|
sensors = []
|
||||||
for entry in config[CONF_INPUTS]:
|
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):
|
class ModbusBinarySensor(BinarySensorDevice):
|
||||||
@ -107,15 +107,17 @@ class ModbusBinarySensor(BinarySensorDevice):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
async def async_update(self):
|
def update(self):
|
||||||
"""Update the state of the sensor."""
|
"""Update the state of the sensor."""
|
||||||
|
try:
|
||||||
if self._input_type == CALL_TYPE_COIL:
|
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:
|
else:
|
||||||
result = await self._hub.read_discrete_inputs(self._slave, self._address, 1)
|
result = self._hub.read_discrete_inputs(self._slave, self._address, 1)
|
||||||
if result is None:
|
except ConnectionException:
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
|
@ -3,7 +3,7 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pymodbus.exceptions import ModbusException
|
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||||
from pymodbus.pdu import ExceptionResponse
|
from pymodbus.pdu import ExceptionResponse
|
||||||
import voluptuous as vol
|
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."""
|
"""Set up the Modbus Thermostat Platform."""
|
||||||
name = config[CONF_NAME]
|
name = config[CONF_NAME]
|
||||||
modbus_slave = config[CONF_SLAVE]
|
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_name = config[CONF_HUB]
|
||||||
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
||||||
|
|
||||||
async_add_entities(
|
add_entities(
|
||||||
[
|
[
|
||||||
ModbusThermostat(
|
ModbusThermostat(
|
||||||
hub,
|
hub,
|
||||||
@ -170,12 +170,12 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
return SUPPORT_TARGET_TEMPERATURE
|
return SUPPORT_TARGET_TEMPERATURE
|
||||||
|
|
||||||
async def async_update(self):
|
def update(self):
|
||||||
"""Update Target & Current Temperature."""
|
"""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
|
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
|
self._current_temperature_register_type, self._current_temperature_register
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
"""Return the supported step of target temperature."""
|
"""Return the supported step of target temperature."""
|
||||||
return self._temp_step
|
return self._temp_step
|
||||||
|
|
||||||
async def set_temperature(self, **kwargs):
|
def set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
target_temperature = int(
|
target_temperature = int(
|
||||||
(kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale
|
(kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale
|
||||||
@ -233,26 +233,28 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
return
|
return
|
||||||
byte_string = struct.pack(self._structure, target_temperature)
|
byte_string = struct.pack(self._structure, target_temperature)
|
||||||
register_value = struct.unpack(">h", byte_string[0:2])[0]
|
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
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._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."""
|
"""Read register using the Modbus hub slave."""
|
||||||
|
try:
|
||||||
if register_type == CALL_TYPE_REGISTER_INPUT:
|
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
|
self._slave, register, self._count
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await self._hub.read_holding_registers(
|
result = self._hub.read_holding_registers(
|
||||||
self._slave, register, self._count
|
self._slave, register, self._count
|
||||||
)
|
)
|
||||||
if result is None:
|
except ConnectionException:
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
@ -269,7 +271,12 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
|
|
||||||
return register_value
|
return register_value
|
||||||
|
|
||||||
async def _write_register(self, register, value):
|
def _write_register(self, register, value):
|
||||||
"""Write holding register using the Modbus hub slave."""
|
"""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
|
self._available = True
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
"domain": "modbus",
|
"domain": "modbus",
|
||||||
"name": "Modbus",
|
"name": "Modbus",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
||||||
"requirements": ["pymodbus==2.3.0",
|
"requirements": ["pymodbus==2.3.0"],
|
||||||
"pyserial-asyncio==0.4"],
|
|
||||||
"codeowners": ["@adamchengtkc", "@janiversen"]
|
"codeowners": ["@adamchengtkc", "@janiversen"]
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import logging
|
|||||||
import struct
|
import struct
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from pymodbus.exceptions import ModbusException
|
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||||
from pymodbus.pdu import ExceptionResponse
|
from pymodbus.pdu import ExceptionResponse
|
||||||
import voluptuous as vol
|
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."""
|
"""Set up the Modbus sensors."""
|
||||||
sensors = []
|
sensors = []
|
||||||
data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}}
|
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:
|
if not sensors:
|
||||||
return False
|
return False
|
||||||
async_add_entities(sensors)
|
add_entities(sensors)
|
||||||
|
|
||||||
|
|
||||||
class ModbusRegisterSensor(RestoreEntity):
|
class ModbusRegisterSensor(RestoreEntity):
|
||||||
@ -219,19 +219,21 @@ class ModbusRegisterSensor(RestoreEntity):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
async def async_update(self):
|
def update(self):
|
||||||
"""Update the state of the sensor."""
|
"""Update the state of the sensor."""
|
||||||
|
try:
|
||||||
if self._register_type == CALL_TYPE_REGISTER_INPUT:
|
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
|
self._slave, self._register, self._count
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await self._hub.read_holding_registers(
|
result = self._hub.read_holding_registers(
|
||||||
self._slave, self._register, self._count
|
self._slave, self._register, self._count
|
||||||
)
|
)
|
||||||
if result is None:
|
except ConnectionException:
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
@ -246,7 +248,7 @@ class ModbusRegisterSensor(RestoreEntity):
|
|||||||
if isinstance(val, int):
|
if isinstance(val, int):
|
||||||
self._value = str(val)
|
self._value = str(val)
|
||||||
if self._precision > 0:
|
if self._precision > 0:
|
||||||
self._value += f".{'0' * self._precision}"
|
self._value += "." + "0" * self._precision
|
||||||
else:
|
else:
|
||||||
self._value = f"{val:.{self._precision}f}"
|
self._value = f"{val:.{self._precision}f}"
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pymodbus.exceptions import ModbusException
|
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||||
from pymodbus.pdu import ExceptionResponse
|
from pymodbus.pdu import ExceptionResponse
|
||||||
import voluptuous as vol
|
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."""
|
"""Read configuration and create Modbus devices."""
|
||||||
switches = []
|
switches = []
|
||||||
if CONF_COILS in config:
|
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):
|
class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
|
||||||
@ -146,24 +146,26 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
async def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Set switch on."""
|
"""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."""
|
"""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."""
|
"""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."""
|
"""Read coil using the Modbus hub slave."""
|
||||||
result = await self._hub.read_coils(self._slave, coil, 1)
|
try:
|
||||||
if result is None:
|
result = self._hub.read_coils(self._slave, coil, 1)
|
||||||
|
except ConnectionException:
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
@ -173,9 +175,14 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
async def _write_coil(self, coil, value):
|
def _write_coil(self, coil, value):
|
||||||
"""Write coil using the Modbus hub slave."""
|
"""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
|
self._available = True
|
||||||
|
|
||||||
|
|
||||||
@ -221,21 +228,21 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
|
|
||||||
self._is_on = None
|
self._is_on = None
|
||||||
|
|
||||||
async def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Set switch on."""
|
"""Set switch on."""
|
||||||
|
|
||||||
# Only holding register is writable
|
# Only holding register is writable
|
||||||
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
|
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:
|
if not self._verify_state:
|
||||||
self._is_on = True
|
self._is_on = True
|
||||||
|
|
||||||
async def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Set switch off."""
|
"""Set switch off."""
|
||||||
|
|
||||||
# Only holding register is writable
|
# Only holding register is writable
|
||||||
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
|
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:
|
if not self._verify_state:
|
||||||
self._is_on = False
|
self._is_on = False
|
||||||
|
|
||||||
@ -244,12 +251,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
async def async_update(self):
|
def update(self):
|
||||||
"""Update the state of the switch."""
|
"""Update the state of the switch."""
|
||||||
if not self._verify_state:
|
if not self._verify_state:
|
||||||
return
|
return
|
||||||
|
|
||||||
value = await self._read_register()
|
value = self._read_register()
|
||||||
if value == self._state_on:
|
if value == self._state_on:
|
||||||
self._is_on = True
|
self._is_on = True
|
||||||
elif value == self._state_off:
|
elif value == self._state_off:
|
||||||
@ -263,18 +270,18 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
value,
|
value,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _read_register(self) -> Optional[int]:
|
def _read_register(self) -> Optional[int]:
|
||||||
|
try:
|
||||||
if self._register_type == CALL_TYPE_REGISTER_INPUT:
|
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, 1)
|
||||||
self._slave, self._register, 1
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
result = await self._hub.read_holding_registers(
|
result = self._hub.read_holding_registers(
|
||||||
self._slave, self._register, 1
|
self._slave, self._register, 1
|
||||||
)
|
)
|
||||||
if result is None:
|
except ConnectionException:
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||||
self._available = False
|
self._available = False
|
||||||
return
|
return
|
||||||
@ -284,7 +291,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
async def _write_register(self, value):
|
def _write_register(self, value):
|
||||||
"""Write holding register using the Modbus hub slave."""
|
"""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
|
self._available = True
|
||||||
|
@ -1531,7 +1531,6 @@ pysdcp==1
|
|||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.0.3
|
pysensibo==1.0.3
|
||||||
|
|
||||||
# homeassistant.components.modbus
|
|
||||||
# homeassistant.components.serial
|
# homeassistant.components.serial
|
||||||
pyserial-asyncio==0.4
|
pyserial-asyncio==0.4
|
||||||
|
|
||||||
|
@ -600,10 +600,6 @@ pyps4-2ndscreen==1.0.7
|
|||||||
# homeassistant.components.qwikswitch
|
# homeassistant.components.qwikswitch
|
||||||
pyqwikswitch==0.93
|
pyqwikswitch==0.93
|
||||||
|
|
||||||
# homeassistant.components.modbus
|
|
||||||
# homeassistant.components.serial
|
|
||||||
pyserial-asyncio==0.4
|
|
||||||
|
|
||||||
# homeassistant.components.signal_messenger
|
# homeassistant.components.signal_messenger
|
||||||
pysignalclirestapi==0.2.4
|
pysignalclirestapi==0.2.4
|
||||||
|
|
||||||
|
@ -40,19 +40,11 @@ class ReadResult:
|
|||||||
self.registers = register_words
|
self.registers = register_words
|
||||||
|
|
||||||
|
|
||||||
read_result = None
|
|
||||||
|
|
||||||
|
|
||||||
async def run_test(
|
async def run_test(
|
||||||
hass, use_mock_hub, register_config, entity_domain, register_words, expected
|
hass, use_mock_hub, register_config, entity_domain, register_words, expected
|
||||||
):
|
):
|
||||||
"""Run test for given config and check that sensor outputs expected result."""
|
"""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
|
# Full sensor configuration
|
||||||
sensor_name = "modbus_test_sensor"
|
sensor_name = "modbus_test_sensor"
|
||||||
scan_interval = 5
|
scan_interval = 5
|
||||||
@ -69,9 +61,9 @@ async def run_test(
|
|||||||
# Setup inputs for the sensor
|
# Setup inputs for the sensor
|
||||||
read_result = ReadResult(register_words)
|
read_result = ReadResult(register_words)
|
||||||
if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT:
|
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:
|
else:
|
||||||
use_mock_hub.read_holding_registers = simulate_read_registers
|
use_mock_hub.read_holding_registers.return_value = read_result
|
||||||
|
|
||||||
# Initialize sensor
|
# Initialize sensor
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user