mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
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:
parent
188ca630de
commit
dd3cd95954
@ -231,7 +231,7 @@ homeassistant/components/min_max/* @fabaff
|
||||
homeassistant/components/minecraft_server/* @elmurato
|
||||
homeassistant/components/minio/* @tkislan
|
||||
homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen
|
||||
homeassistant/components/monoprice/* @etsinko
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
|
@ -1,13 +1,19 @@
|
||||
"""Support for Modbus."""
|
||||
import asyncio
|
||||
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
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_STATE,
|
||||
CONF_DELAY,
|
||||
CONF_HOST,
|
||||
CONF_METHOD,
|
||||
CONF_NAME,
|
||||
@ -19,24 +25,26 @@ from homeassistant.const import (
|
||||
)
|
||||
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"
|
||||
ATTR_HUB = "hub"
|
||||
ATTR_UNIT = "unit"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
CONF_BAUDRATE = "baudrate"
|
||||
CONF_BYTESIZE = "bytesize"
|
||||
# Kept for compatibility with other integrations, TO BE REMOVED
|
||||
CONF_HUB = "hub"
|
||||
CONF_PARITY = "parity"
|
||||
CONF_STOPBITS = "stopbits"
|
||||
|
||||
DEFAULT_HUB = "default"
|
||||
DOMAIN = "modbus"
|
||||
DOMAIN = MODBUS_DOMAIN
|
||||
|
||||
SERVICE_WRITE_COIL = "write_coil"
|
||||
SERVICE_WRITE_REGISTER = "write_register"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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_TYPE): vol.Any("tcp", "udp", "rtuovertcp"),
|
||||
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
|
||||
vol.Optional(CONF_DELAY, default=0): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@ -88,97 +97,65 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup_client(client_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):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up Modbus component."""
|
||||
hass.data[DOMAIN] = hub_collect = {}
|
||||
hass.data[MODBUS_DOMAIN] = hub_collect = {}
|
||||
|
||||
for client_config in config[DOMAIN]:
|
||||
client = setup_client(client_config)
|
||||
name = client_config[CONF_NAME]
|
||||
hub_collect[name] = ModbusHub(client, name)
|
||||
_LOGGER.debug("Setting up hub: %s", client_config)
|
||||
_LOGGER.debug("registering hubs")
|
||||
for client_config in config[MODBUS_DOMAIN]:
|
||||
hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop)
|
||||
|
||||
def stop_modbus(event):
|
||||
"""Stop Modbus service."""
|
||||
for client in hub_collect.values():
|
||||
client.close()
|
||||
del client
|
||||
|
||||
def start_modbus(event):
|
||||
"""Start Modbus service."""
|
||||
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)
|
||||
|
||||
# Register services for modbus
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
hass.services.async_register(
|
||||
MODBUS_DOMAIN,
|
||||
SERVICE_WRITE_REGISTER,
|
||||
write_register,
|
||||
schema=SERVICE_WRITE_REGISTER_SCHEMA,
|
||||
)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA
|
||||
hass.services.async_register(
|
||||
MODBUS_DOMAIN,
|
||||
SERVICE_WRITE_COIL,
|
||||
write_coil,
|
||||
schema=SERVICE_WRITE_COIL_SCHEMA,
|
||||
)
|
||||
|
||||
def write_register(service):
|
||||
async 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):
|
||||
hub_collect[client_name].write_registers(
|
||||
await hub_collect[client_name].write_registers(
|
||||
unit, address, [int(float(i)) for i in value]
|
||||
)
|
||||
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."""
|
||||
unit = service.data[ATTR_UNIT]
|
||||
address = service.data[ATTR_ADDRESS]
|
||||
state = service.data[ATTR_STATE]
|
||||
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
|
||||
|
||||
@ -186,65 +163,153 @@ def setup(hass, config):
|
||||
class ModbusHub:
|
||||
"""Thread safe wrapper class for pymodbus."""
|
||||
|
||||
def __init__(self, modbus_client, name):
|
||||
def __init__(self, client_config, main_loop):
|
||||
"""Initialize the Modbus hub."""
|
||||
self._client = modbus_client
|
||||
self._lock = threading.Lock()
|
||||
self._name = name
|
||||
_LOGGER.debug("Preparing setup: %s", client_config)
|
||||
|
||||
# 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
|
||||
def name(self):
|
||||
"""Return the name of this hub."""
|
||||
return self._name
|
||||
return self._config_name
|
||||
|
||||
def close(self):
|
||||
"""Disconnect client."""
|
||||
with self._lock:
|
||||
self._client.close()
|
||||
async def _connect_delay(self):
|
||||
if self._config_delay > 0:
|
||||
await asyncio.sleep(self._config_delay)
|
||||
self._config_delay = 0
|
||||
|
||||
def connect(self):
|
||||
"""Connect client."""
|
||||
with self._lock:
|
||||
self._client.connect()
|
||||
def setup(self):
|
||||
"""Set up pymodbus client."""
|
||||
# pylint: disable = E0633
|
||||
# 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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
return self._client.read_coils(address, count, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
return self._client.read_discrete_inputs(address, count, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
return self._client.read_input_registers(address, count, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
return self._client.read_holding_registers(address, count, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
self._client.write_coil(address, value, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
self._client.write_register(address, value, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
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."""
|
||||
with self._lock:
|
||||
kwargs = {"unit": unit} if unit else {}
|
||||
self._client.write_registers(address, values, **kwargs)
|
||||
if self._client.protocol is None:
|
||||
return None
|
||||
return await self._write(
|
||||
unit, address, values, self._client.protocol.write_registers
|
||||
)
|
||||
|
@ -2,7 +2,7 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
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.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__)
|
||||
|
||||
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(
|
||||
cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS),
|
||||
cv.deprecated(CONF_COILS, CONF_INPUTS),
|
||||
PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_INPUTS): [
|
||||
vol.All(
|
||||
cv.deprecated(CONF_DEPRECATED_COIL, CONF_ADDRESS),
|
||||
cv.deprecated(CALL_TYPE_COIL, CONF_ADDRESS),
|
||||
vol.Schema(
|
||||
{
|
||||
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_SLAVE): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL
|
||||
): vol.In(
|
||||
[DEFAULT_INPUT_TYPE_COIL, DEFAULT_INPUT_TYPE_DISCRETE]
|
||||
),
|
||||
CONF_INPUT_TYPE, default=CALL_TYPE_COIL
|
||||
): vol.In([CALL_TYPE_COIL, CALL_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."""
|
||||
sensors = []
|
||||
for entry in config[CONF_INPUTS]:
|
||||
@ -109,33 +107,18 @@ class ModbusBinarySensor(BinarySensorDevice):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update the state of the sensor."""
|
||||
try:
|
||||
if self._input_type == DEFAULT_INPUT_TYPE_COIL:
|
||||
result = self._hub.read_coils(self._slave, self._address, 1)
|
||||
else:
|
||||
result = self._hub.read_discrete_inputs(self._slave, self._address, 1)
|
||||
except ConnectionException:
|
||||
self._set_unavailable()
|
||||
if self._input_type == CALL_TYPE_COIL:
|
||||
result = await 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:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||
self._set_unavailable()
|
||||
self._available = False
|
||||
return
|
||||
|
||||
self._value = result.bits[0]
|
||||
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
|
||||
|
@ -3,7 +3,7 @@ import logging
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
import voluptuous as vol
|
||||
|
||||
@ -21,30 +21,31 @@ from homeassistant.const import (
|
||||
)
|
||||
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__)
|
||||
|
||||
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(
|
||||
{
|
||||
@ -52,10 +53,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_SLAVE): 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(
|
||||
CONF_CURRENT_TEMP_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING
|
||||
): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]),
|
||||
CONF_CURRENT_TEMP_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING
|
||||
): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
|
||||
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In(
|
||||
[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."""
|
||||
name = config[CONF_NAME]
|
||||
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_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE]
|
||||
data_type = config[CONF_DATA_TYPE]
|
||||
count = config[CONF_COUNT]
|
||||
count = config[CONF_DATA_COUNT]
|
||||
precision = config[CONF_PRECISION]
|
||||
scale = config[CONF_SCALE]
|
||||
offset = config[CONF_OFFSET]
|
||||
@ -167,14 +168,14 @@ class ModbusThermostat(ClimateDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""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."""
|
||||
self._target_temperature = self._read_register(
|
||||
DEFAULT_REGISTER_TYPE_HOLDING, self._target_temperature_register
|
||||
self._target_temperature = await self._read_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
|
||||
)
|
||||
|
||||
@ -186,7 +187,7 @@ class ModbusThermostat(ClimateDevice):
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the possible HVAC modes."""
|
||||
return HVAC_MODES
|
||||
return [HVAC_MODE_AUTO]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -223,7 +224,7 @@ class ModbusThermostat(ClimateDevice):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._temp_step
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
async def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temperature = int(
|
||||
(kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale
|
||||
@ -232,30 +233,28 @@ class ModbusThermostat(ClimateDevice):
|
||||
return
|
||||
byte_string = struct.pack(self._structure, target_temperature)
|
||||
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
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is 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."""
|
||||
try:
|
||||
if register_type == DEFAULT_REGISTER_TYPE_INPUT:
|
||||
result = self._hub.read_input_registers(
|
||||
self._slave, register, self._count
|
||||
)
|
||||
else:
|
||||
result = self._hub.read_holding_registers(
|
||||
self._slave, register, self._count
|
||||
)
|
||||
except ConnectionException:
|
||||
self._set_unavailable(register)
|
||||
if register_type == CALL_TYPE_REGISTER_INPUT:
|
||||
result = await self._hub.read_input_registers(
|
||||
self._slave, register, self._count
|
||||
)
|
||||
else:
|
||||
result = await self._hub.read_holding_registers(
|
||||
self._slave, register, self._count
|
||||
)
|
||||
if result is None:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||
self._set_unavailable(register)
|
||||
self._available = False
|
||||
return
|
||||
|
||||
byte_string = b"".join(
|
||||
@ -270,25 +269,7 @@ class ModbusThermostat(ClimateDevice):
|
||||
|
||||
return register_value
|
||||
|
||||
def _write_register(self, register, value):
|
||||
async def _write_register(self, register, value):
|
||||
"""Write holding register using the Modbus hub slave."""
|
||||
try:
|
||||
self._hub.write_registers(self._slave, register, [value, 0])
|
||||
except ConnectionException:
|
||||
self._set_unavailable(register)
|
||||
return
|
||||
|
||||
await self._hub.write_registers(self._slave, register, [value, 0])
|
||||
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
|
||||
|
72
homeassistant/components/modbus/const.py
Normal file
72
homeassistant/components/modbus/const.py
Normal 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"
|
@ -2,7 +2,7 @@
|
||||
"domain": "modbus",
|
||||
"name": "Modbus",
|
||||
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
||||
"requirements": ["pymodbus==1.5.2"],
|
||||
"requirements": ["pymodbus==2.3.0"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@adamchengtkc"]
|
||||
"codeowners": ["@adamchengtkc", "@janiversen"]
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import logging
|
||||
import struct
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
import voluptuous as vol
|
||||
|
||||
@ -19,27 +19,28 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
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__)
|
||||
|
||||
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]:
|
||||
"""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_PRECISION, default=0): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING
|
||||
): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]),
|
||||
CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING
|
||||
): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]),
|
||||
vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SCALE, default=1): number,
|
||||
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."""
|
||||
sensors = []
|
||||
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 self._available
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update the state of the sensor."""
|
||||
try:
|
||||
if self._register_type == DEFAULT_REGISTER_TYPE_INPUT:
|
||||
result = self._hub.read_input_registers(
|
||||
self._slave, self._register, self._count
|
||||
)
|
||||
else:
|
||||
result = self._hub.read_holding_registers(
|
||||
self._slave, self._register, self._count
|
||||
)
|
||||
except ConnectionException:
|
||||
self._set_unavailable()
|
||||
if self._register_type == CALL_TYPE_REGISTER_INPUT:
|
||||
result = await self._hub.read_input_registers(
|
||||
self._slave, self._register, self._count
|
||||
)
|
||||
else:
|
||||
result = await self._hub.read_holding_registers(
|
||||
self._slave, self._register, self._count
|
||||
)
|
||||
if result is None:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||
self._set_unavailable()
|
||||
self._available = False
|
||||
return
|
||||
|
||||
registers = result.registers
|
||||
@ -252,16 +251,3 @@ class ModbusRegisterSensor(RestoreEntity):
|
||||
self._value = f"{val:.{self._precision}f}"
|
||||
|
||||
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
|
||||
|
@ -2,7 +2,7 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from pymodbus.exceptions import ConnectionException, ModbusException
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
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.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__)
|
||||
|
||||
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(
|
||||
{
|
||||
@ -42,8 +45,8 @@ REGISTERS_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_REGISTER): cv.positive_int,
|
||||
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
||||
vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In(
|
||||
[DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]
|
||||
vol.Optional(CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In(
|
||||
[CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]
|
||||
),
|
||||
vol.Optional(CONF_SLAVE): cv.positive_int,
|
||||
vol.Optional(CONF_STATE_OFF): cv.positive_int,
|
||||
@ -55,7 +58,7 @@ REGISTERS_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_SLAVE): cv.positive_int,
|
||||
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."""
|
||||
switches = []
|
||||
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]
|
||||
switches.append(
|
||||
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:
|
||||
@ -143,28 +146,26 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
async def turn_on(self, **kwargs):
|
||||
"""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."""
|
||||
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."""
|
||||
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."""
|
||||
try:
|
||||
result = self._hub.read_coils(self._slave, coil, 1)
|
||||
except ConnectionException:
|
||||
self._set_unavailable()
|
||||
result = await self._hub.read_coils(self._slave, coil, 1)
|
||||
if result is None:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||
self._set_unavailable()
|
||||
self._available = False
|
||||
return
|
||||
|
||||
value = bool(result.bits[0])
|
||||
@ -172,29 +173,11 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
|
||||
|
||||
return value
|
||||
|
||||
def _write_coil(self, coil, value):
|
||||
async def _write_coil(self, coil, value):
|
||||
"""Write coil using the Modbus hub slave."""
|
||||
try:
|
||||
self._hub.write_coil(self._slave, coil, value)
|
||||
except ConnectionException:
|
||||
self._set_unavailable()
|
||||
return
|
||||
|
||||
await self._hub.write_coil(self._slave, coil, value)
|
||||
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):
|
||||
"""Representation of a Modbus register switch."""
|
||||
@ -238,21 +221,21 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
||||
|
||||
self._is_on = None
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
async def turn_on(self, **kwargs):
|
||||
"""Set switch on."""
|
||||
|
||||
# Only holding register is writable
|
||||
if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING:
|
||||
self._write_register(self._command_on)
|
||||
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
|
||||
await self._write_register(self._command_on)
|
||||
if not self._verify_state:
|
||||
self._is_on = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
async def turn_off(self, **kwargs):
|
||||
"""Set switch off."""
|
||||
|
||||
# Only holding register is writable
|
||||
if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING:
|
||||
self._write_register(self._command_off)
|
||||
if self._register_type == CALL_TYPE_REGISTER_HOLDING:
|
||||
await self._write_register(self._command_off)
|
||||
if not self._verify_state:
|
||||
self._is_on = False
|
||||
|
||||
@ -261,12 +244,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update the state of the switch."""
|
||||
if not self._verify_state:
|
||||
return
|
||||
|
||||
value = self._read_register()
|
||||
value = await self._read_register()
|
||||
if value == self._state_on:
|
||||
self._is_on = True
|
||||
elif value == self._state_off:
|
||||
@ -280,20 +263,20 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
||||
value,
|
||||
)
|
||||
|
||||
def _read_register(self) -> Optional[int]:
|
||||
try:
|
||||
if self._register_type == DEFAULT_REGISTER_TYPE_INPUT:
|
||||
result = self._hub.read_input_registers(self._slave, self._register, 1)
|
||||
else:
|
||||
result = self._hub.read_holding_registers(
|
||||
self._slave, self._register, 1
|
||||
)
|
||||
except ConnectionException:
|
||||
self._set_unavailable()
|
||||
async def _read_register(self) -> Optional[int]:
|
||||
if self._register_type == CALL_TYPE_REGISTER_INPUT:
|
||||
result = await self._hub.read_input_registers(
|
||||
self._slave, self._register, 1
|
||||
)
|
||||
else:
|
||||
result = await self._hub.read_holding_registers(
|
||||
self._slave, self._register, 1
|
||||
)
|
||||
if result is None:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
if isinstance(result, (ModbusException, ExceptionResponse)):
|
||||
self._set_unavailable()
|
||||
self._available = False
|
||||
return
|
||||
|
||||
value = int(result.registers[0])
|
||||
@ -301,25 +284,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
||||
|
||||
return value
|
||||
|
||||
def _write_register(self, value):
|
||||
async def _write_register(self, value):
|
||||
"""Write holding register using the Modbus hub slave."""
|
||||
try:
|
||||
self._hub.write_register(self._slave, self._register, value)
|
||||
except ConnectionException:
|
||||
self._set_unavailable()
|
||||
return
|
||||
|
||||
await self._hub.write_register(self._slave, self._register, value)
|
||||
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
|
||||
|
@ -1399,7 +1399,7 @@ pymitv==1.4.3
|
||||
pymochad==0.2.0
|
||||
|
||||
# homeassistant.components.modbus
|
||||
pymodbus==1.5.2
|
||||
pymodbus==2.3.0
|
||||
|
||||
# homeassistant.components.monoprice
|
||||
pymonoprice==0.3
|
||||
|
@ -543,7 +543,7 @@ pymfy==0.7.1
|
||||
pymochad==0.2.0
|
||||
|
||||
# homeassistant.components.modbus
|
||||
pymodbus==1.5.2
|
||||
pymodbus==2.3.0
|
||||
|
||||
# homeassistant.components.monoprice
|
||||
pymonoprice==0.3
|
||||
|
@ -4,10 +4,12 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN
|
||||
from homeassistant.components.modbus.sensor import (
|
||||
from homeassistant.components.modbus.const import (
|
||||
CALL_TYPE_REGISTER_HOLDING,
|
||||
CALL_TYPE_REGISTER_INPUT,
|
||||
CONF_COUNT,
|
||||
CONF_DATA_TYPE,
|
||||
CONF_OFFSET,
|
||||
CONF_PRECISION,
|
||||
CONF_REGISTER,
|
||||
CONF_REGISTER_TYPE,
|
||||
@ -17,16 +19,10 @@ from homeassistant.components.modbus.sensor import (
|
||||
DATA_TYPE_FLOAT,
|
||||
DATA_TYPE_INT,
|
||||
DATA_TYPE_UINT,
|
||||
DEFAULT_REGISTER_TYPE_HOLDING,
|
||||
DEFAULT_REGISTER_TYPE_INPUT,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_OFFSET,
|
||||
CONF_PLATFORM,
|
||||
CONF_SCAN_INTERVAL,
|
||||
DEFAULT_HUB,
|
||||
MODBUS_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL
|
||||
from homeassistant.setup import async_setup_component
|
||||
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"
|
||||
scan_interval = 5
|
||||
config = {
|
||||
SENSOR_DOMAIN: {
|
||||
MODBUS_DOMAIN: {
|
||||
CONF_PLATFORM: "modbus",
|
||||
CONF_SCAN_INTERVAL: scan_interval,
|
||||
CONF_REGISTERS: [
|
||||
@ -72,7 +68,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected):
|
||||
|
||||
# Setup inputs for the sensor
|
||||
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
|
||||
else:
|
||||
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
|
||||
now = dt_util.utcnow()
|
||||
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
|
||||
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)
|
||||
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):
|
||||
"""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."""
|
||||
register_config = {
|
||||
CONF_COUNT: 2,
|
||||
CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_INPUT,
|
||||
CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT,
|
||||
CONF_DATA_TYPE: DATA_TYPE_UINT,
|
||||
CONF_SCALE: 1,
|
||||
CONF_OFFSET: 0,
|
||||
@ -329,7 +320,7 @@ async def test_two_word_holding_register(hass, mock_hub):
|
||||
"""Test reaging of holding register."""
|
||||
register_config = {
|
||||
CONF_COUNT: 2,
|
||||
CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING,
|
||||
CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
|
||||
CONF_DATA_TYPE: DATA_TYPE_UINT,
|
||||
CONF_SCALE: 1,
|
||||
CONF_OFFSET: 0,
|
||||
@ -348,7 +339,7 @@ async def test_float_data_type(hass, mock_hub):
|
||||
"""Test floating point register data type."""
|
||||
register_config = {
|
||||
CONF_COUNT: 2,
|
||||
CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING,
|
||||
CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING,
|
||||
CONF_DATA_TYPE: DATA_TYPE_FLOAT,
|
||||
CONF_SCALE: 1,
|
||||
CONF_OFFSET: 0,
|
||||
|
Loading…
x
Reference in New Issue
Block a user