mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +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/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
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
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",
|
"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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user