Add service to stop/restart modbus (#55599)
* Add service to stop/restart modbus. Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
31623368c8
commit
aaa62dadec
7 changed files with 180 additions and 28 deletions
|
@ -369,6 +369,11 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
SERVICE_STOP_START_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_HUB): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_hub(hass: HomeAssistant, name: str) -> ModbusHub:
|
def get_hub(hass: HomeAssistant, name: str) -> ModbusHub:
|
||||||
|
@ -379,5 +384,9 @@ def get_hub(hass: HomeAssistant, name: str) -> ModbusHub:
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up Modbus component."""
|
"""Set up Modbus component."""
|
||||||
return await async_modbus_setup(
|
return await async_modbus_setup(
|
||||||
hass, config, SERVICE_WRITE_REGISTER_SCHEMA, SERVICE_WRITE_COIL_SCHEMA
|
hass,
|
||||||
|
config,
|
||||||
|
SERVICE_WRITE_REGISTER_SCHEMA,
|
||||||
|
SERVICE_WRITE_COIL_SCHEMA,
|
||||||
|
SERVICE_STOP_START_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from abc import abstractmethod
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
|
@ -21,6 +21,8 @@ from homeassistant.const import (
|
||||||
CONF_STRUCTURE,
|
CONF_STRUCTURE,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity, ToggleEntity
|
from homeassistant.helpers.entity import Entity, ToggleEntity
|
||||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
@ -50,6 +52,8 @@ from .const import (
|
||||||
CONF_VERIFY,
|
CONF_VERIFY,
|
||||||
CONF_WRITE_TYPE,
|
CONF_WRITE_TYPE,
|
||||||
DATA_TYPE_STRING,
|
DATA_TYPE_STRING,
|
||||||
|
SIGNAL_START_ENTITY,
|
||||||
|
SIGNAL_STOP_ENTITY,
|
||||||
)
|
)
|
||||||
from .modbus import ModbusHub
|
from .modbus import ModbusHub
|
||||||
|
|
||||||
|
@ -73,6 +77,7 @@ class BasePlatform(Entity):
|
||||||
self._value = None
|
self._value = None
|
||||||
self._scan_interval = int(entry[CONF_SCAN_INTERVAL])
|
self._scan_interval = int(entry[CONF_SCAN_INTERVAL])
|
||||||
self._call_active = False
|
self._call_active = False
|
||||||
|
self._cancel_timer: Callable[[], None] | None = None
|
||||||
|
|
||||||
self._attr_name = entry[CONF_NAME]
|
self._attr_name = entry[CONF_NAME]
|
||||||
self._attr_should_poll = False
|
self._attr_should_poll = False
|
||||||
|
@ -86,13 +91,35 @@ class BasePlatform(Entity):
|
||||||
async def async_update(self, now=None):
|
async def async_update(self, now=None):
|
||||||
"""Virtual function to be overwritten."""
|
"""Virtual function to be overwritten."""
|
||||||
|
|
||||||
async def async_base_added_to_hass(self):
|
@callback
|
||||||
"""Handle entity which will be added."""
|
def async_remote_start(self) -> None:
|
||||||
|
"""Remote start entity."""
|
||||||
|
if self._cancel_timer:
|
||||||
|
self._cancel_timer()
|
||||||
|
self._cancel_timer = None
|
||||||
if self._scan_interval > 0:
|
if self._scan_interval > 0:
|
||||||
cancel_func = async_track_time_interval(
|
self._cancel_timer = async_track_time_interval(
|
||||||
self.hass, self.async_update, timedelta(seconds=self._scan_interval)
|
self.hass, self.async_update, timedelta(seconds=self._scan_interval)
|
||||||
)
|
)
|
||||||
self._hub.entity_timers.append(cancel_func)
|
self._attr_available = True
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remote_stop(self) -> None:
|
||||||
|
"""Remote stop entity."""
|
||||||
|
if self._cancel_timer:
|
||||||
|
self._cancel_timer()
|
||||||
|
self._cancel_timer = None
|
||||||
|
self._attr_available = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_base_added_to_hass(self):
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
self.async_remote_start()
|
||||||
|
async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_remote_stop)
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, SIGNAL_START_ENTITY, self.async_remote_start
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseStructPlatform(BasePlatform, RestoreEntity):
|
class BaseStructPlatform(BasePlatform, RestoreEntity):
|
||||||
|
|
|
@ -106,6 +106,12 @@ CALL_TYPE_X_REGISTER_HOLDINGS = "holdings"
|
||||||
# service calls
|
# service calls
|
||||||
SERVICE_WRITE_COIL = "write_coil"
|
SERVICE_WRITE_COIL = "write_coil"
|
||||||
SERVICE_WRITE_REGISTER = "write_register"
|
SERVICE_WRITE_REGISTER = "write_register"
|
||||||
|
SERVICE_STOP = "stop"
|
||||||
|
SERVICE_RESTART = "restart"
|
||||||
|
|
||||||
|
# dispatcher signals
|
||||||
|
SIGNAL_STOP_ENTITY = "modbus.stop"
|
||||||
|
SIGNAL_START_ENTITY = "modbus.start"
|
||||||
|
|
||||||
# integration names
|
# integration names
|
||||||
DEFAULT_HUB = "modbus_hub"
|
DEFAULT_HUB = "modbus_hub"
|
||||||
|
|
|
@ -74,6 +74,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
|
||||||
self._status_register_type = config[CONF_STATUS_REGISTER_TYPE]
|
self._status_register_type = config[CONF_STATUS_REGISTER_TYPE]
|
||||||
|
|
||||||
self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||||
|
self._attr_is_closed = False
|
||||||
|
|
||||||
# If we read cover status from coil, and not from optional status register,
|
# If we read cover status from coil, and not from optional status register,
|
||||||
# we interpret boolean value False as closed cover, and value True as open cover.
|
# we interpret boolean value False as closed cover, and value True as open cover.
|
||||||
|
|
|
@ -20,8 +20,9 @@ from homeassistant.const import (
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
@ -51,8 +52,12 @@ from .const import (
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
RTUOVERTCP,
|
RTUOVERTCP,
|
||||||
SERIAL,
|
SERIAL,
|
||||||
|
SERVICE_RESTART,
|
||||||
|
SERVICE_STOP,
|
||||||
SERVICE_WRITE_COIL,
|
SERVICE_WRITE_COIL,
|
||||||
SERVICE_WRITE_REGISTER,
|
SERVICE_WRITE_REGISTER,
|
||||||
|
SIGNAL_START_ENTITY,
|
||||||
|
SIGNAL_STOP_ENTITY,
|
||||||
TCP,
|
TCP,
|
||||||
UDP,
|
UDP,
|
||||||
)
|
)
|
||||||
|
@ -107,7 +112,11 @@ PYMODBUS_CALL = [
|
||||||
|
|
||||||
|
|
||||||
async def async_modbus_setup(
|
async def async_modbus_setup(
|
||||||
hass, config, service_write_register_schema, service_write_coil_schema
|
hass,
|
||||||
|
config,
|
||||||
|
service_write_register_schema,
|
||||||
|
service_write_coil_schema,
|
||||||
|
service_stop_start_schema,
|
||||||
):
|
):
|
||||||
"""Set up Modbus component."""
|
"""Set up Modbus component."""
|
||||||
|
|
||||||
|
@ -131,9 +140,9 @@ async def async_modbus_setup(
|
||||||
async def async_stop_modbus(event):
|
async def async_stop_modbus(event):
|
||||||
"""Stop Modbus service."""
|
"""Stop Modbus service."""
|
||||||
|
|
||||||
|
async_dispatcher_send(hass, SIGNAL_STOP_ENTITY)
|
||||||
for client in hub_collect.values():
|
for client in hub_collect.values():
|
||||||
await client.async_close()
|
await client.async_close()
|
||||||
del client
|
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_modbus)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_modbus)
|
||||||
|
|
||||||
|
@ -142,15 +151,15 @@ async def async_modbus_setup(
|
||||||
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 = (
|
hub = hub_collect[
|
||||||
service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB
|
service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB
|
||||||
)
|
]
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
await hub_collect[client_name].async_pymodbus_call(
|
await hub.async_pymodbus_call(
|
||||||
unit, address, [int(float(i)) for i in value], CALL_TYPE_WRITE_REGISTERS
|
unit, address, [int(float(i)) for i in value], CALL_TYPE_WRITE_REGISTERS
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await hub_collect[client_name].async_pymodbus_call(
|
await hub.async_pymodbus_call(
|
||||||
unit, address, int(float(value)), CALL_TYPE_WRITE_REGISTER
|
unit, address, int(float(value)), CALL_TYPE_WRITE_REGISTER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -166,35 +175,48 @@ async def async_modbus_setup(
|
||||||
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 = (
|
hub = hub_collect[
|
||||||
service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB
|
service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB
|
||||||
)
|
]
|
||||||
if isinstance(state, list):
|
if isinstance(state, list):
|
||||||
await hub_collect[client_name].async_pymodbus_call(
|
await hub.async_pymodbus_call(unit, address, state, CALL_TYPE_WRITE_COILS)
|
||||||
unit, address, state, CALL_TYPE_WRITE_COILS
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await hub_collect[client_name].async_pymodbus_call(
|
await hub.async_pymodbus_call(unit, address, state, CALL_TYPE_WRITE_COIL)
|
||||||
unit, address, state, CALL_TYPE_WRITE_COIL
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_WRITE_COIL, async_write_coil, schema=service_write_coil_schema
|
DOMAIN, SERVICE_WRITE_COIL, async_write_coil, schema=service_write_coil_schema
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_stop_hub(service):
|
||||||
|
"""Stop Modbus hub."""
|
||||||
|
async_dispatcher_send(hass, SIGNAL_STOP_ENTITY)
|
||||||
|
hub = hub_collect[service.data[ATTR_HUB]]
|
||||||
|
await hub.async_close()
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_STOP, async_stop_hub, schema=service_stop_start_schema
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_restart_hub(service):
|
||||||
|
"""Restart Modbus hub."""
|
||||||
|
async_dispatcher_send(hass, SIGNAL_START_ENTITY)
|
||||||
|
hub = hub_collect[service.data[ATTR_HUB]]
|
||||||
|
await hub.async_restart()
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_RESTART, async_restart_hub, schema=service_stop_start_schema
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ModbusHub:
|
class ModbusHub:
|
||||||
"""Thread safe wrapper class for pymodbus."""
|
"""Thread safe wrapper class for pymodbus."""
|
||||||
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __init__(self, hass, client_config):
|
def __init__(self, hass, client_config):
|
||||||
"""Initialize the Modbus hub."""
|
"""Initialize the Modbus hub."""
|
||||||
|
|
||||||
# generic configuration
|
# generic configuration
|
||||||
self._client = None
|
self._client = None
|
||||||
self.entity_timers: list[CALLBACK_TYPE] = []
|
|
||||||
self._async_cancel_listener = None
|
self._async_cancel_listener = None
|
||||||
self._in_error = False
|
self._in_error = False
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
@ -284,29 +306,40 @@ class ModbusHub:
|
||||||
self._async_cancel_listener = None
|
self._async_cancel_listener = None
|
||||||
self._config_delay = 0
|
self._config_delay = 0
|
||||||
|
|
||||||
|
async def async_restart(self):
|
||||||
|
"""Reconnect client."""
|
||||||
|
if self._client:
|
||||||
|
await self.async_close()
|
||||||
|
|
||||||
|
await self.async_setup()
|
||||||
|
|
||||||
async def async_close(self):
|
async def async_close(self):
|
||||||
"""Disconnect client."""
|
"""Disconnect client."""
|
||||||
if self._async_cancel_listener:
|
if self._async_cancel_listener:
|
||||||
self._async_cancel_listener()
|
self._async_cancel_listener()
|
||||||
self._async_cancel_listener = None
|
self._async_cancel_listener = None
|
||||||
for call in self.entity_timers:
|
|
||||||
call()
|
|
||||||
self.entity_timers = []
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if self._client:
|
if self._client:
|
||||||
try:
|
try:
|
||||||
self._client.close()
|
self._client.close()
|
||||||
except ModbusException as exception_error:
|
except ModbusException as exception_error:
|
||||||
self._log_error(str(exception_error))
|
self._log_error(str(exception_error))
|
||||||
|
del self._client
|
||||||
self._client = None
|
self._client = None
|
||||||
|
message = f"modbus {self.name} communication closed"
|
||||||
|
_LOGGER.warning(message)
|
||||||
|
|
||||||
def _pymodbus_connect(self):
|
def _pymodbus_connect(self):
|
||||||
"""Connect client."""
|
"""Connect client."""
|
||||||
try:
|
try:
|
||||||
return self._client.connect()
|
self._client.connect()
|
||||||
except ModbusException as exception_error:
|
except ModbusException as exception_error:
|
||||||
self._log_error(str(exception_error), error_state=False)
|
self._log_error(str(exception_error), error_state=False)
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
message = f"modbus {self.name} communication open"
|
||||||
|
_LOGGER.warning(message)
|
||||||
|
return True
|
||||||
|
|
||||||
def _pymodbus_call(self, unit, address, value, use_call):
|
def _pymodbus_call(self, unit, address, value, use_call):
|
||||||
"""Call sync. pymodbus."""
|
"""Call sync. pymodbus."""
|
||||||
|
|
|
@ -66,3 +66,25 @@ write_register:
|
||||||
default: "modbus_hub"
|
default: "modbus_hub"
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
stop:
|
||||||
|
name: Stop
|
||||||
|
description: Stop modbus hub.
|
||||||
|
fields:
|
||||||
|
hub:
|
||||||
|
name: Hub
|
||||||
|
description: Modbus hub name.
|
||||||
|
example: "hub1"
|
||||||
|
default: "modbus_hub"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
restart:
|
||||||
|
name: Restart
|
||||||
|
description: Restart modbus hub (if running stop then start).
|
||||||
|
fields:
|
||||||
|
hub:
|
||||||
|
name: Hub
|
||||||
|
description: Modbus hub name.
|
||||||
|
example: "hub1"
|
||||||
|
default: "modbus_hub"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
|
|
@ -53,6 +53,8 @@ from homeassistant.components.modbus.const import (
|
||||||
MODBUS_DOMAIN as DOMAIN,
|
MODBUS_DOMAIN as DOMAIN,
|
||||||
RTUOVERTCP,
|
RTUOVERTCP,
|
||||||
SERIAL,
|
SERIAL,
|
||||||
|
SERVICE_RESTART,
|
||||||
|
SERVICE_STOP,
|
||||||
SERVICE_WRITE_COIL,
|
SERVICE_WRITE_COIL,
|
||||||
SERVICE_WRITE_REGISTER,
|
SERVICE_WRITE_REGISTER,
|
||||||
TCP,
|
TCP,
|
||||||
|
@ -82,6 +84,7 @@ from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
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
|
||||||
|
@ -715,3 +718,54 @@ async def test_shutdown(hass, caplog, mock_pymodbus, mock_modbus_with_pymodbus):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert mock_pymodbus.close.called
|
assert mock_pymodbus.close.called
|
||||||
assert caplog.text == ""
|
assert caplog.text == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"do_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_ADDRESS: 51,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_stop_restart(hass, caplog, mock_modbus):
|
||||||
|
"""Run test for service stop."""
|
||||||
|
|
||||||
|
entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}"
|
||||||
|
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||||
|
hass.states.async_set(entity_id, 17)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get(entity_id).state == "17"
|
||||||
|
|
||||||
|
mock_modbus.reset_mock()
|
||||||
|
caplog.clear()
|
||||||
|
data = {
|
||||||
|
ATTR_HUB: TEST_MODBUS_NAME,
|
||||||
|
}
|
||||||
|
await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||||
|
assert mock_modbus.close.called
|
||||||
|
assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text
|
||||||
|
|
||||||
|
mock_modbus.reset_mock()
|
||||||
|
caplog.clear()
|
||||||
|
await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not mock_modbus.close.called
|
||||||
|
assert mock_modbus.connect.called
|
||||||
|
assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text
|
||||||
|
|
||||||
|
mock_modbus.reset_mock()
|
||||||
|
caplog.clear()
|
||||||
|
await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_modbus.close.called
|
||||||
|
assert mock_modbus.connect.called
|
||||||
|
assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text
|
||||||
|
assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue