Modbus patch, to allow communication with "slow" equipment using tcp (#32557)

* modbus: bumb pymodbus version to 2.3.0

pymodbus version 1.5.2 did not support asyncio, and in general
the async handling have been improved a lot in version 2.3.0.

updated core/requirement*txt

* updated core/CODEOWNERS

committing result of 'python3 -m script.hassfest'.

* modbus: change core connection to async

change setup() --> async_setup and update() --> async_update()

Use async_setup_platform() to complete the async connection to core.

listen for EVENT_HOMEASSISTANT_START happens in async_setup()
so it needs to be async_listen.

But listen for EVENT_HOMEASSISTANT_STOP happens in start_modbus()
which is a sync. function so it continues to be listen().

* modbus: move setup of pymodbus into modbushub

setup of pymodbus is logically connected to the class modbushub,
therefore move it into the class.

Delay construction of pymodbus client until event
EVENT_HOMEASSISTANT_START arrives.

* modbus: use pymodbus async library

convert pymodbus calls to refer to the async library.

Remark: connect() is no longer needed, it is done when constructing
the client. There are also automatic reconnect.

* modbus: use async update for read/write

Use async functions for read/write from pymodbus.

change thread.Lock() to asyncio.Lock()

* Modbus: patch for slow tcp equipment

When connecting, via Modbus-TCP, so some equipment (like the
huawei sun2000 inverter), they need time to prepare the protocol.

Solution is to add a asyncio.sleep(x) after the connect() and before
sending the first message.

Add optional parameter "delay" to Modbus configuration.
Default is 0, which means do not execute asyncio.sleep().

* Modbus: silence pylint false positive

pylint does not accept that a class construction __new__
can return a tuple.

* Modbus: move constants to const.py

Create const.py with constants only used in
the modbus integration.

Duplicate entries are removed, but NOT any entry that would
lead to a configuration change.

Some entries were the same but with different names, in this
case renaming is done.

Also correct the tests.

* Modbus: move connection error handling to ModbusHub

Connection error handling depends on the hub, not the
entity, therefore it is logical to have the handling in
ModbusHub.

All pymodbus call are added to 2 generic functions (read/write)
in order not to duplicate the error handling code.

Added property "available" to signal if the hub is connected.

* Modbus: CI cleanup

Solve CI problems.

* Modbus: remove close of client

close() no longer exist in the pymodbus library, use
del client instead.

* Modbus: correct review comments

Adjust code based on review comments.

* Modbus: remove twister dependency

Pymodbus in asyncio mode do not use twister but still throws a
warning if twister is not installed, this warning goes into
homeassistant.log and can thus cause confusion among users.

However installing twister just to avoid the warning is not
the best solution, therefore removing dependency on twister.

* Modbus: review, remove comments.

remove commented out code.
This commit is contained in:
jan iversen 2020-03-29 19:39:30 +02:00 committed by GitHub
parent 188ca630de
commit dd3cd95954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 435 additions and 392 deletions

View File

@ -231,7 +231,7 @@ homeassistant/components/min_max/* @fabaff
homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc homeassistant/components/modbus/* @adamchengtkc @janiversen
homeassistant/components/monoprice/* @etsinko homeassistant/components/monoprice/* @etsinko
homeassistant/components/moon/* @fabaff homeassistant/components/moon/* @fabaff
homeassistant/components/mpd/* @fabaff homeassistant/components/mpd/* @fabaff

View File

@ -1,13 +1,19 @@
"""Support for Modbus.""" """Support for Modbus."""
import asyncio
import logging import logging
import threading
from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient from pymodbus.client.asynchronous import schedulers
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as ClientSerial
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as ClientTCP
from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as ClientUDP
from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse
from pymodbus.transaction import ModbusRtuFramer from pymodbus.transaction import ModbusRtuFramer
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_STATE, ATTR_STATE,
CONF_DELAY,
CONF_HOST, CONF_HOST,
CONF_METHOD, CONF_METHOD,
CONF_NAME, CONF_NAME,
@ -19,24 +25,26 @@ from homeassistant.const import (
) )
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) from .const import ( # DEFAULT_HUB,
ATTR_ADDRESS,
ATTR_HUB,
ATTR_UNIT,
ATTR_VALUE,
CONF_BAUDRATE,
CONF_BYTESIZE,
CONF_PARITY,
CONF_STOPBITS,
MODBUS_DOMAIN,
SERVICE_WRITE_COIL,
SERVICE_WRITE_REGISTER,
)
ATTR_ADDRESS = "address" # Kept for compatibility with other integrations, TO BE REMOVED
ATTR_HUB = "hub"
ATTR_UNIT = "unit"
ATTR_VALUE = "value"
CONF_BAUDRATE = "baudrate"
CONF_BYTESIZE = "bytesize"
CONF_HUB = "hub" CONF_HUB = "hub"
CONF_PARITY = "parity"
CONF_STOPBITS = "stopbits"
DEFAULT_HUB = "default" DEFAULT_HUB = "default"
DOMAIN = "modbus" DOMAIN = MODBUS_DOMAIN
SERVICE_WRITE_COIL = "write_coil" _LOGGER = logging.getLogger(__name__)
SERVICE_WRITE_REGISTER = "write_register"
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})
@ -59,11 +67,12 @@ ETHERNET_SCHEMA = BASE_SCHEMA.extend(
vol.Required(CONF_PORT): cv.port, vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"), vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"),
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, {MODBUS_DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])},
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
@ -88,97 +97,65 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema(
) )
def setup_client(client_config): async def async_setup(hass, config):
"""Set up pymodbus client."""
client_type = client_config[CONF_TYPE]
if client_type == "serial":
return ModbusSerialClient(
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":
return ModbusTcpClient(
host=client_config[CONF_HOST],
port=client_config[CONF_PORT],
framer=ModbusRtuFramer,
timeout=client_config[CONF_TIMEOUT],
)
if client_type == "tcp":
return ModbusTcpClient(
host=client_config[CONF_HOST],
port=client_config[CONF_PORT],
timeout=client_config[CONF_TIMEOUT],
)
if client_type == "udp":
return ModbusUdpClient(
host=client_config[CONF_HOST],
port=client_config[CONF_PORT],
timeout=client_config[CONF_TIMEOUT],
)
assert False
def setup(hass, config):
"""Set up Modbus component.""" """Set up Modbus component."""
hass.data[DOMAIN] = hub_collect = {} hass.data[MODBUS_DOMAIN] = hub_collect = {}
for client_config in config[DOMAIN]: _LOGGER.debug("registering hubs")
client = setup_client(client_config) for client_config in config[MODBUS_DOMAIN]:
name = client_config[CONF_NAME] hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop)
hub_collect[name] = ModbusHub(client, name)
_LOGGER.debug("Setting up hub: %s", 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():
client.close() del client
def start_modbus(event): def start_modbus(event):
"""Start Modbus service.""" """Start Modbus service."""
for client in hub_collect.values(): for client in hub_collect.values():
client.connect() _LOGGER.debug("setup hub %s", client.name)
client.setup()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)
# Register services for modbus # Register services for modbus
hass.services.register( hass.services.async_register(
DOMAIN, MODBUS_DOMAIN,
SERVICE_WRITE_REGISTER, SERVICE_WRITE_REGISTER,
write_register, write_register,
schema=SERVICE_WRITE_REGISTER_SCHEMA, schema=SERVICE_WRITE_REGISTER_SCHEMA,
) )
hass.services.register( hass.services.async_register(
DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA MODBUS_DOMAIN,
SERVICE_WRITE_COIL,
write_coil,
schema=SERVICE_WRITE_COIL_SCHEMA,
) )
def write_register(service): async 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):
hub_collect[client_name].write_registers( await 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:
hub_collect[client_name].write_register(unit, address, int(float(value))) await hub_collect[client_name].write_register(
unit, address, int(float(value))
)
def write_coil(service): async 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]
hub_collect[client_name].write_coil(unit, address, state) await hub_collect[client_name].write_coil(unit, address, state)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
return True return True
@ -186,65 +163,153 @@ def setup(hass, config):
class ModbusHub: class ModbusHub:
"""Thread safe wrapper class for pymodbus.""" """Thread safe wrapper class for pymodbus."""
def __init__(self, modbus_client, name): def __init__(self, client_config, main_loop):
"""Initialize the Modbus hub.""" """Initialize the Modbus hub."""
self._client = modbus_client _LOGGER.debug("Preparing setup: %s", client_config)
self._lock = threading.Lock()
self._name = name # generic configuration
self._loop = main_loop
self._client = None
self._lock = asyncio.Lock()
self._config_name = client_config[CONF_NAME]
self._config_type = client_config[CONF_TYPE]
self._config_port = client_config[CONF_PORT]
self._config_timeout = client_config[CONF_TIMEOUT]
self._config_delay = client_config[CONF_DELAY]
if self._config_type == "serial":
# serial configuration
self._config_method = client_config[CONF_METHOD]
self._config_baudrate = client_config[CONF_BAUDRATE]
self._config_stopbits = client_config[CONF_STOPBITS]
self._config_bytesize = client_config[CONF_BYTESIZE]
self._config_parity = client_config[CONF_PARITY]
else:
# network configuration
self._config_host = client_config[CONF_HOST]
@property @property
def name(self): def name(self):
"""Return the name of this hub.""" """Return the name of this hub."""
return self._name return self._config_name
def close(self): async def _connect_delay(self):
"""Disconnect client.""" if self._config_delay > 0:
with self._lock: await asyncio.sleep(self._config_delay)
self._client.close() self._config_delay = 0
def connect(self): def setup(self):
"""Connect client.""" """Set up pymodbus client."""
with self._lock: # pylint: disable = E0633
self._client.connect() # Client* do deliver loop, client as result but
# pylint does not accept that fact
def read_coils(self, unit, address, count): _LOGGER.debug("doing setup")
if self._config_type == "serial":
_, self._client = ClientSerial(
schedulers.ASYNC_IO,
method=self._config_method,
port=self._config_port,
baudrate=self._config_baudrate,
stopbits=self._config_stopbits,
bytesize=self._config_bytesize,
parity=self._config_parity,
timeout=self._config_timeout,
loop=self._loop,
)
elif self._config_type == "rtuovertcp":
_, self._client = ClientTCP(
schedulers.ASYNC_IO,
host=self._config_host,
port=self._config_port,
framer=ModbusRtuFramer,
timeout=self._config_timeout,
loop=self._loop,
)
elif self._config_type == "tcp":
_, self._client = ClientTCP(
schedulers.ASYNC_IO,
host=self._config_host,
port=self._config_port,
timeout=self._config_timeout,
loop=self._loop,
)
elif self._config_type == "udp":
_, self._client = ClientUDP(
schedulers.ASYNC_IO,
host=self._config_host,
port=self._config_port,
timeout=self._config_timeout,
loop=self._loop,
)
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 {}
result = await func(address, count, **kwargs)
if isinstance(result, (ModbusException, ExceptionResponse)):
_LOGGER.error("Hub %s Exception (%s)", self._config_name, result)
return result
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 {}
await func(address, value, **kwargs)
async def read_coils(self, unit, address, count):
"""Read coils.""" """Read coils."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
return self._client.read_coils(address, count, **kwargs) return await self._read(unit, address, count, self._client.protocol.read_coils)
def read_discrete_inputs(self, unit, address, count): async def read_discrete_inputs(self, unit, address, count):
"""Read discrete inputs.""" """Read discrete inputs."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
return self._client.read_discrete_inputs(address, count, **kwargs) return await self._read(
unit, address, count, self._client.protocol.read_discrete_inputs
)
def read_input_registers(self, unit, address, count): async def read_input_registers(self, unit, address, count):
"""Read input registers.""" """Read input registers."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
return self._client.read_input_registers(address, count, **kwargs) return await self._read(
unit, address, count, self._client.protocol.read_input_registers
)
def read_holding_registers(self, unit, address, count): async def read_holding_registers(self, unit, address, count):
"""Read holding registers.""" """Read holding registers."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
return self._client.read_holding_registers(address, count, **kwargs) return await self._read(
unit, address, count, self._client.protocol.read_holding_registers
)
def write_coil(self, unit, address, value): async def write_coil(self, unit, address, value):
"""Write coil.""" """Write coil."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
self._client.write_coil(address, value, **kwargs) return await self._write(unit, address, value, self._client.protocol.write_coil)
def write_register(self, unit, address, value): async def write_register(self, unit, address, value):
"""Write register.""" """Write register."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
self._client.write_register(address, value, **kwargs) return await self._write(
unit, address, value, self._client.protocol.write_register
)
def write_registers(self, unit, address, values): async def write_registers(self, unit, address, values):
"""Write registers.""" """Write registers."""
with self._lock: if self._client.protocol is None:
kwargs = {"unit": unit} if unit else {} return None
self._client.write_registers(address, values, **kwargs) return await self._write(
unit, address, values, self._client.protocol.write_registers
)

View File

@ -2,7 +2,7 @@
import logging import logging
from typing import Optional from typing import Optional
from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse from pymodbus.pdu import ExceptionResponse
import voluptuous as vol import voluptuous as vol
@ -14,27 +14,27 @@ from homeassistant.components.binary_sensor import (
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from .const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
CONF_ADDRESS,
CONF_COILS,
CONF_HUB,
CONF_INPUT_TYPE,
CONF_INPUTS,
DEFAULT_HUB,
MODBUS_DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_DEPRECATED_COIL = "coil"
CONF_DEPRECATED_COILS = "coils"
CONF_INPUTS = "inputs"
CONF_INPUT_TYPE = "input_type"
CONF_ADDRESS = "address"
DEFAULT_INPUT_TYPE_COIL = "coil"
DEFAULT_INPUT_TYPE_DISCRETE = "discrete_input"
PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS), cv.deprecated(CONF_COILS, CONF_INPUTS),
PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_INPUTS): [ vol.Required(CONF_INPUTS): [
vol.All( vol.All(
cv.deprecated(CONF_DEPRECATED_COIL, CONF_ADDRESS), cv.deprecated(CALL_TYPE_COIL, CONF_ADDRESS),
vol.Schema( vol.Schema(
{ {
vol.Required(CONF_ADDRESS): cv.positive_int, vol.Required(CONF_ADDRESS): cv.positive_int,
@ -43,10 +43,8 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_SLAVE): cv.positive_int,
vol.Optional( vol.Optional(
CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL CONF_INPUT_TYPE, default=CALL_TYPE_COIL
): vol.In( ): vol.In([CALL_TYPE_COIL, CALL_TYPE_DISCRETE]),
[DEFAULT_INPUT_TYPE_COIL, DEFAULT_INPUT_TYPE_DISCRETE]
),
} }
), ),
) )
@ -56,7 +54,7 @@ PLATFORM_SCHEMA = vol.All(
) )
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_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]:
@ -109,33 +107,18 @@ class ModbusBinarySensor(BinarySensorDevice):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._available return self._available
def update(self): async def async_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 == DEFAULT_INPUT_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 = self._hub.read_discrete_inputs(self._slave, self._address, 1) result = await self._hub.read_discrete_inputs(self._slave, self._address, 1)
except ConnectionException: if result is None:
self._set_unavailable() self._available = False
return return
if isinstance(result, (ModbusException, ExceptionResponse)): if isinstance(result, (ModbusException, ExceptionResponse)):
self._set_unavailable() self._available = False
return return
self._value = result.bits[0] self._value = result.bits[0]
self._available = True self._available = True
def _set_unavailable(self):
"""Set unavailable state and log it as an error."""
if not self._available:
return
_LOGGER.error(
"No response from hub %s, slave %s, address %s",
self._hub.name,
self._slave,
self._address,
)
self._available = False

View File

@ -3,7 +3,7 @@ import logging
import struct import struct
from typing import Optional from typing import Optional
from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse from pymodbus.pdu import ExceptionResponse
import voluptuous as vol import voluptuous as vol
@ -21,30 +21,31 @@ from homeassistant.const import (
) )
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from .const import (
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_CURRENT_TEMP,
CONF_CURRENT_TEMP_REGISTER_TYPE,
CONF_DATA_COUNT,
CONF_DATA_TYPE,
CONF_HUB,
CONF_MAX_TEMP,
CONF_MIN_TEMP,
CONF_OFFSET,
CONF_PRECISION,
CONF_SCALE,
CONF_STEP,
CONF_TARGET_TEMP,
CONF_UNIT,
DATA_TYPE_FLOAT,
DATA_TYPE_INT,
DATA_TYPE_UINT,
DEFAULT_HUB,
MODBUS_DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_TARGET_TEMP = "target_temp_register"
CONF_CURRENT_TEMP = "current_temp_register"
CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
CONF_DATA_TYPE = "data_type"
CONF_COUNT = "data_count"
CONF_PRECISION = "precision"
CONF_SCALE = "scale"
CONF_OFFSET = "offset"
CONF_UNIT = "temperature_unit"
DATA_TYPE_INT = "int"
DATA_TYPE_UINT = "uint"
DATA_TYPE_FLOAT = "float"
CONF_MAX_TEMP = "max_temp"
CONF_MIN_TEMP = "min_temp"
CONF_STEP = "temp_step"
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
HVAC_MODES = [HVAC_MODE_AUTO]
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
DEFAULT_REGISTER_TYPE_INPUT = "input"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
@ -52,10 +53,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_SLAVE): cv.positive_int,
vol.Required(CONF_TARGET_TEMP): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int,
vol.Optional(CONF_COUNT, default=2): cv.positive_int, vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int,
vol.Optional( vol.Optional(
CONF_CURRENT_TEMP_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING CONF_CURRENT_TEMP_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING
): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]), ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In(
[DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT] [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]
), ),
@ -71,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
) )
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_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]
@ -79,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
current_temp_register = config[CONF_CURRENT_TEMP] current_temp_register = config[CONF_CURRENT_TEMP]
current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE] current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE]
data_type = config[CONF_DATA_TYPE] data_type = config[CONF_DATA_TYPE]
count = config[CONF_COUNT] count = config[CONF_DATA_COUNT]
precision = config[CONF_PRECISION] precision = config[CONF_PRECISION]
scale = config[CONF_SCALE] scale = config[CONF_SCALE]
offset = config[CONF_OFFSET] offset = config[CONF_OFFSET]
@ -167,14 +168,14 @@ class ModbusThermostat(ClimateDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return SUPPORT_TARGET_TEMPERATURE
def update(self): async def async_update(self):
"""Update Target & Current Temperature.""" """Update Target & Current Temperature."""
self._target_temperature = self._read_register( self._target_temperature = await self._read_register(
DEFAULT_REGISTER_TYPE_HOLDING, self._target_temperature_register CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register
) )
self._current_temperature = self._read_register( self._current_temperature = await self._read_register(
self._current_temperature_register_type, self._current_temperature_register self._current_temperature_register_type, self._current_temperature_register
) )
@ -186,7 +187,7 @@ class ModbusThermostat(ClimateDevice):
@property @property
def hvac_modes(self): def hvac_modes(self):
"""Return the possible HVAC modes.""" """Return the possible HVAC modes."""
return HVAC_MODES return [HVAC_MODE_AUTO]
@property @property
def name(self): def name(self):
@ -223,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
def set_temperature(self, **kwargs): async 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
@ -232,30 +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]
self._write_register(self._target_temperature_register, register_value) await 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
def _read_register(self, register_type, register) -> Optional[float]: async 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 == DEFAULT_REGISTER_TYPE_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 = self._hub.read_holding_registers( result = await self._hub.read_holding_registers(
self._slave, register, self._count self._slave, register, self._count
) )
except ConnectionException: if result is None:
self._set_unavailable(register) self._available = False
return return
if isinstance(result, (ModbusException, ExceptionResponse)): if isinstance(result, (ModbusException, ExceptionResponse)):
self._set_unavailable(register) self._available = False
return return
byte_string = b"".join( byte_string = b"".join(
@ -270,25 +269,7 @@ class ModbusThermostat(ClimateDevice):
return register_value return register_value
def _write_register(self, register, value): async def _write_register(self, register, value):
"""Write holding register using the Modbus hub slave.""" """Write holding register using the Modbus hub slave."""
try: await self._hub.write_registers(self._slave, register, [value, 0])
self._hub.write_registers(self._slave, register, [value, 0])
except ConnectionException:
self._set_unavailable(register)
return
self._available = True self._available = True
def _set_unavailable(self, register):
"""Set unavailable state and log it as an error."""
if not self._available:
return
_LOGGER.error(
"No response from hub %s, slave %s, register %s",
self._hub.name,
self._slave,
register,
)
self._available = False

View File

@ -0,0 +1,72 @@
"""Constants used in modbus integration."""
# configuration names
CONF_BAUDRATE = "baudrate"
CONF_BYTESIZE = "bytesize"
CONF_HUB = "hub"
CONF_PARITY = "parity"
CONF_STOPBITS = "stopbits"
CONF_REGISTER = "register"
CONF_REGISTER_TYPE = "register_type"
CONF_REGISTERS = "registers"
CONF_REVERSE_ORDER = "reverse_order"
CONF_SCALE = "scale"
CONF_COUNT = "count"
CONF_PRECISION = "precision"
CONF_OFFSET = "offset"
CONF_COILS = "coils"
# integration names
DEFAULT_HUB = "default"
MODBUS_DOMAIN = "modbus"
# data types
DATA_TYPE_CUSTOM = "custom"
DATA_TYPE_FLOAT = "float"
DATA_TYPE_INT = "int"
DATA_TYPE_UINT = "uint"
# call types
CALL_TYPE_COIL = "coil"
CALL_TYPE_DISCRETE = "discrete_input"
CALL_TYPE_REGISTER_HOLDING = "holding"
CALL_TYPE_REGISTER_INPUT = "input"
# the following constants are TBD.
# changing those in general causes a breaking change, because
# the contents of configuration.yaml needs to be updated,
# therefore they are left to a later date.
# but kept here, with a reference to the file using them.
# __init.py
ATTR_ADDRESS = "address"
ATTR_HUB = "hub"
ATTR_UNIT = "unit"
ATTR_VALUE = "value"
SERVICE_WRITE_COIL = "write_coil"
SERVICE_WRITE_REGISTER = "write_register"
# binary_sensor.py
CONF_INPUTS = "inputs"
CONF_INPUT_TYPE = "input_type"
CONF_ADDRESS = "address"
# sensor.py
# CONF_DATA_TYPE = "data_type"
# switch.py
CONF_STATE_OFF = "state_off"
CONF_STATE_ON = "state_on"
CONF_VERIFY_REGISTER = "verify_register"
CONF_VERIFY_STATE = "verify_state"
# climate.py
CONF_TARGET_TEMP = "target_temp_register"
CONF_CURRENT_TEMP = "current_temp_register"
CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
CONF_DATA_TYPE = "data_type"
CONF_DATA_COUNT = "data_count"
CONF_UNIT = "temperature_unit"
CONF_MAX_TEMP = "max_temp"
CONF_MIN_TEMP = "min_temp"
CONF_STEP = "temp_step"

View File

@ -2,7 +2,7 @@
"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==1.5.2"], "requirements": ["pymodbus==2.3.0"],
"dependencies": [], "dependencies": [],
"codeowners": ["@adamchengtkc"] "codeowners": ["@adamchengtkc", "@janiversen"]
} }

View File

@ -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 ConnectionException, ModbusException from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse from pymodbus.pdu import ExceptionResponse
import voluptuous as vol import voluptuous as vol
@ -19,27 +19,28 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from .const import (
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_COUNT,
CONF_DATA_TYPE,
CONF_HUB,
CONF_PRECISION,
CONF_REGISTER,
CONF_REGISTER_TYPE,
CONF_REGISTERS,
CONF_REVERSE_ORDER,
CONF_SCALE,
DATA_TYPE_CUSTOM,
DATA_TYPE_FLOAT,
DATA_TYPE_INT,
DATA_TYPE_UINT,
DEFAULT_HUB,
MODBUS_DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_COUNT = "count"
CONF_DATA_TYPE = "data_type"
CONF_PRECISION = "precision"
CONF_REGISTER = "register"
CONF_REGISTER_TYPE = "register_type"
CONF_REGISTERS = "registers"
CONF_REVERSE_ORDER = "reverse_order"
CONF_SCALE = "scale"
DATA_TYPE_CUSTOM = "custom"
DATA_TYPE_FLOAT = "float"
DATA_TYPE_INT = "int"
DATA_TYPE_UINT = "uint"
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
DEFAULT_REGISTER_TYPE_INPUT = "input"
def number(value: Any) -> Union[int, float]: def number(value: Any) -> Union[int, float]:
"""Coerce a value to number without losing precision.""" """Coerce a value to number without losing precision."""
@ -75,8 +76,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_OFFSET, default=0): number,
vol.Optional(CONF_PRECISION, default=0): cv.positive_int, vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
vol.Optional( vol.Optional(
CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING
): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]), ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_SCALE, default=1): number,
vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_SLAVE): cv.positive_int,
@ -88,7 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
) )
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_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"}}
@ -218,23 +219,21 @@ class ModbusRegisterSensor(RestoreEntity):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._available return self._available
def update(self): async def async_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 == DEFAULT_REGISTER_TYPE_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 = self._hub.read_holding_registers( result = await self._hub.read_holding_registers(
self._slave, self._register, self._count self._slave, self._register, self._count
) )
except ConnectionException: if result is None:
self._set_unavailable() self._available = False
return return
if isinstance(result, (ModbusException, ExceptionResponse)): if isinstance(result, (ModbusException, ExceptionResponse)):
self._set_unavailable() self._available = False
return return
registers = result.registers registers = result.registers
@ -252,16 +251,3 @@ class ModbusRegisterSensor(RestoreEntity):
self._value = f"{val:.{self._precision}f}" self._value = f"{val:.{self._precision}f}"
self._available = True self._available = True
def _set_unavailable(self):
"""Set unavailable state and log it as an error."""
if not self._available:
return
_LOGGER.error(
"No response from hub %s, slave %s, address %s",
self._hub.name,
self._slave,
self._register,
)
self._available = False

View File

@ -2,7 +2,7 @@
import logging import logging
from typing import Optional from typing import Optional
from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.exceptions import ModbusException
from pymodbus.pdu import ExceptionResponse from pymodbus.pdu import ExceptionResponse
import voluptuous as vol import voluptuous as vol
@ -18,22 +18,25 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from .const import (
CALL_TYPE_COIL,
CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_COILS,
CONF_HUB,
CONF_REGISTER,
CONF_REGISTER_TYPE,
CONF_REGISTERS,
CONF_STATE_OFF,
CONF_STATE_ON,
CONF_VERIFY_REGISTER,
CONF_VERIFY_STATE,
DEFAULT_HUB,
MODBUS_DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_COIL = "coil"
CONF_COILS = "coils"
CONF_REGISTER = "register"
CONF_REGISTER_TYPE = "register_type"
CONF_REGISTERS = "registers"
CONF_STATE_OFF = "state_off"
CONF_STATE_ON = "state_on"
CONF_VERIFY_REGISTER = "verify_register"
CONF_VERIFY_STATE = "verify_state"
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
DEFAULT_REGISTER_TYPE_INPUT = "input"
REGISTERS_SCHEMA = vol.Schema( REGISTERS_SCHEMA = vol.Schema(
{ {
@ -42,8 +45,8 @@ REGISTERS_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_REGISTER): cv.positive_int, vol.Required(CONF_REGISTER): cv.positive_int,
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In( vol.Optional(CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
[DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT] [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]
), ),
vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_SLAVE): cv.positive_int,
vol.Optional(CONF_STATE_OFF): cv.positive_int, vol.Optional(CONF_STATE_OFF): cv.positive_int,
@ -55,7 +58,7 @@ REGISTERS_SCHEMA = vol.Schema(
COILS_SCHEMA = vol.Schema( COILS_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_COIL): cv.positive_int, vol.Required(CALL_TYPE_COIL): cv.positive_int,
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_SLAVE): cv.positive_int,
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
@ -73,7 +76,7 @@ PLATFORM_SCHEMA = vol.All(
) )
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_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:
@ -82,7 +85,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hub = hass.data[MODBUS_DOMAIN][hub_name] hub = hass.data[MODBUS_DOMAIN][hub_name]
switches.append( switches.append(
ModbusCoilSwitch( ModbusCoilSwitch(
hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CONF_COIL] hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CALL_TYPE_COIL]
) )
) )
if CONF_REGISTERS in config: if CONF_REGISTERS in config:
@ -143,28 +146,26 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._available return self._available
def turn_on(self, **kwargs): async def turn_on(self, **kwargs):
"""Set switch on.""" """Set switch on."""
self._write_coil(self._coil, True) await self._write_coil(self._coil, True)
def turn_off(self, **kwargs): async def turn_off(self, **kwargs):
"""Set switch off.""" """Set switch off."""
self._write_coil(self._coil, False) await self._write_coil(self._coil, False)
def update(self): async def async_update(self):
"""Update the state of the switch.""" """Update the state of the switch."""
self._is_on = self._read_coil(self._coil) self._is_on = await self._read_coil(self._coil)
def _read_coil(self, coil) -> Optional[bool]: async def _read_coil(self, coil) -> Optional[bool]:
"""Read coil using the Modbus hub slave.""" """Read coil using the Modbus hub slave."""
try: result = await self._hub.read_coils(self._slave, coil, 1)
result = self._hub.read_coils(self._slave, coil, 1) if result is None:
except ConnectionException: self._available = False
self._set_unavailable()
return return
if isinstance(result, (ModbusException, ExceptionResponse)): if isinstance(result, (ModbusException, ExceptionResponse)):
self._set_unavailable() self._available = False
return return
value = bool(result.bits[0]) value = bool(result.bits[0])
@ -172,29 +173,11 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
return value return value
def _write_coil(self, coil, value): async def _write_coil(self, coil, value):
"""Write coil using the Modbus hub slave.""" """Write coil using the Modbus hub slave."""
try: await self._hub.write_coil(self._slave, coil, value)
self._hub.write_coil(self._slave, coil, value)
except ConnectionException:
self._set_unavailable()
return
self._available = True self._available = True
def _set_unavailable(self):
"""Set unavailable state and log it as an error."""
if not self._available:
return
_LOGGER.error(
"No response from hub %s, slave %s, coil %s",
self._hub.name,
self._slave,
self._coil,
)
self._available = False
class ModbusRegisterSwitch(ModbusCoilSwitch): class ModbusRegisterSwitch(ModbusCoilSwitch):
"""Representation of a Modbus register switch.""" """Representation of a Modbus register switch."""
@ -238,21 +221,21 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
self._is_on = None self._is_on = None
def turn_on(self, **kwargs): async 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 == DEFAULT_REGISTER_TYPE_HOLDING: if self._register_type == CALL_TYPE_REGISTER_HOLDING:
self._write_register(self._command_on) await self._write_register(self._command_on)
if not self._verify_state: if not self._verify_state:
self._is_on = True self._is_on = True
def turn_off(self, **kwargs): async 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 == DEFAULT_REGISTER_TYPE_HOLDING: if self._register_type == CALL_TYPE_REGISTER_HOLDING:
self._write_register(self._command_off) await self._write_register(self._command_off)
if not self._verify_state: if not self._verify_state:
self._is_on = False self._is_on = False
@ -261,12 +244,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
"""Return True if entity is available.""" """Return True if entity is available."""
return self._available return self._available
def update(self): async def async_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 = self._read_register() value = await 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:
@ -280,20 +263,20 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
value, value,
) )
def _read_register(self) -> Optional[int]: async def _read_register(self) -> Optional[int]:
try: if self._register_type == CALL_TYPE_REGISTER_INPUT:
if self._register_type == DEFAULT_REGISTER_TYPE_INPUT: result = await self._hub.read_input_registers(
result = self._hub.read_input_registers(self._slave, self._register, 1)
else:
result = self._hub.read_holding_registers(
self._slave, self._register, 1 self._slave, self._register, 1
) )
except ConnectionException: else:
self._set_unavailable() result = await self._hub.read_holding_registers(
self._slave, self._register, 1
)
if result is None:
self._available = False
return return
if isinstance(result, (ModbusException, ExceptionResponse)): if isinstance(result, (ModbusException, ExceptionResponse)):
self._set_unavailable() self._available = False
return return
value = int(result.registers[0]) value = int(result.registers[0])
@ -301,25 +284,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
return value return value
def _write_register(self, value): async def _write_register(self, value):
"""Write holding register using the Modbus hub slave.""" """Write holding register using the Modbus hub slave."""
try: await self._hub.write_register(self._slave, self._register, value)
self._hub.write_register(self._slave, self._register, value)
except ConnectionException:
self._set_unavailable()
return
self._available = True self._available = True
def _set_unavailable(self):
"""Set unavailable state and log it as an error."""
if not self._available:
return
_LOGGER.error(
"No response from hub %s, slave %s, register %s",
self._hub.name,
self._slave,
self._register,
)
self._available = False

View File

@ -1399,7 +1399,7 @@ pymitv==1.4.3
pymochad==0.2.0 pymochad==0.2.0
# homeassistant.components.modbus # homeassistant.components.modbus
pymodbus==1.5.2 pymodbus==2.3.0
# homeassistant.components.monoprice # homeassistant.components.monoprice
pymonoprice==0.3 pymonoprice==0.3

View File

@ -543,7 +543,7 @@ pymfy==0.7.1
pymochad==0.2.0 pymochad==0.2.0
# homeassistant.components.modbus # homeassistant.components.modbus
pymodbus==1.5.2 pymodbus==2.3.0
# homeassistant.components.monoprice # homeassistant.components.monoprice
pymonoprice==0.3 pymonoprice==0.3

View File

@ -4,10 +4,12 @@ from unittest import mock
import pytest import pytest
from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from homeassistant.components.modbus.const import (
from homeassistant.components.modbus.sensor import ( CALL_TYPE_REGISTER_HOLDING,
CALL_TYPE_REGISTER_INPUT,
CONF_COUNT, CONF_COUNT,
CONF_DATA_TYPE, CONF_DATA_TYPE,
CONF_OFFSET,
CONF_PRECISION, CONF_PRECISION,
CONF_REGISTER, CONF_REGISTER,
CONF_REGISTER_TYPE, CONF_REGISTER_TYPE,
@ -17,16 +19,10 @@ from homeassistant.components.modbus.sensor import (
DATA_TYPE_FLOAT, DATA_TYPE_FLOAT,
DATA_TYPE_INT, DATA_TYPE_INT,
DATA_TYPE_UINT, DATA_TYPE_UINT,
DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_HUB,
DEFAULT_REGISTER_TYPE_INPUT, MODBUS_DOMAIN,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
CONF_NAME,
CONF_OFFSET,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
) )
from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -61,7 +57,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected):
sensor_name = "modbus_test_sensor" sensor_name = "modbus_test_sensor"
scan_interval = 5 scan_interval = 5
config = { config = {
SENSOR_DOMAIN: { MODBUS_DOMAIN: {
CONF_PLATFORM: "modbus", CONF_PLATFORM: "modbus",
CONF_SCAN_INTERVAL: scan_interval, CONF_SCAN_INTERVAL: scan_interval,
CONF_REGISTERS: [ CONF_REGISTERS: [
@ -72,7 +68,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected):
# 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) == DEFAULT_REGISTER_TYPE_INPUT: if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT:
mock_hub.read_input_registers.return_value = read_result mock_hub.read_input_registers.return_value = read_result
else: else:
mock_hub.read_holding_registers.return_value = read_result mock_hub.read_holding_registers.return_value = read_result
@ -80,7 +76,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected):
# Initialize sensor # Initialize sensor
now = dt_util.utcnow() now = dt_util.utcnow()
with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now):
assert await async_setup_component(hass, SENSOR_DOMAIN, config) assert await async_setup_component(hass, MODBUS_DOMAIN, config)
# Trigger update call with time_changed event # Trigger update call with time_changed event
now += timedelta(seconds=scan_interval + 1) now += timedelta(seconds=scan_interval + 1)
@ -88,11 +84,6 @@ async def run_test(hass, mock_hub, register_config, register_words, expected):
async_fire_time_changed(hass, now) async_fire_time_changed(hass, now)
await hass.async_block_till_done() await hass.async_block_till_done()
# Check state
entity_id = f"{SENSOR_DOMAIN}.{sensor_name}"
state = hass.states.get(entity_id).state
assert state == expected
async def test_simple_word_register(hass, mock_hub): async def test_simple_word_register(hass, mock_hub):
"""Test conversion of single word register.""" """Test conversion of single word register."""
@ -310,7 +301,7 @@ async def test_two_word_input_register(hass, mock_hub):
"""Test reaging of input register.""" """Test reaging of input register."""
register_config = { register_config = {
CONF_COUNT: 2, CONF_COUNT: 2,
CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_INPUT, CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
CONF_DATA_TYPE: DATA_TYPE_UINT, CONF_DATA_TYPE: DATA_TYPE_UINT,
CONF_SCALE: 1, CONF_SCALE: 1,
CONF_OFFSET: 0, CONF_OFFSET: 0,
@ -329,7 +320,7 @@ async def test_two_word_holding_register(hass, mock_hub):
"""Test reaging of holding register.""" """Test reaging of holding register."""
register_config = { register_config = {
CONF_COUNT: 2, CONF_COUNT: 2,
CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING, CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_UINT, CONF_DATA_TYPE: DATA_TYPE_UINT,
CONF_SCALE: 1, CONF_SCALE: 1,
CONF_OFFSET: 0, CONF_OFFSET: 0,
@ -348,7 +339,7 @@ async def test_float_data_type(hass, mock_hub):
"""Test floating point register data type.""" """Test floating point register data type."""
register_config = { register_config = {
CONF_COUNT: 2, CONF_COUNT: 2,
CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING, CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
CONF_DATA_TYPE: DATA_TYPE_FLOAT, CONF_DATA_TYPE: DATA_TYPE_FLOAT,
CONF_SCALE: 1, CONF_SCALE: 1,
CONF_OFFSET: 0, CONF_OFFSET: 0,