Add swap byte/word/byteword option to modbus sensor (#49719)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/49744/head
parent
1b957a0ce0
commit
e5e215353d
|
@ -78,6 +78,11 @@ from .const import (
|
|||
CONF_STATUS_REGISTER_TYPE,
|
||||
CONF_STEP,
|
||||
CONF_STOPBITS,
|
||||
CONF_SWAP,
|
||||
CONF_SWAP_BYTE,
|
||||
CONF_SWAP_NONE,
|
||||
CONF_SWAP_WORD,
|
||||
CONF_SWAP_WORD_BYTE,
|
||||
CONF_TARGET_TEMP,
|
||||
CONF_VERIFY_REGISTER,
|
||||
CONF_VERIFY_STATE,
|
||||
|
@ -204,7 +209,10 @@ SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
|||
vol.Optional(CONF_INPUT_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_REVERSE_ORDER): cv.boolean,
|
||||
vol.Optional(CONF_SWAP, default=CONF_SWAP_NONE): vol.In(
|
||||
[CONF_SWAP_NONE, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]
|
||||
),
|
||||
vol.Optional(CONF_SCALE, default=1): number,
|
||||
vol.Optional(CONF_STRUCTURE): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
|
|
|
@ -35,6 +35,11 @@ CONF_STATUS_REGISTER = "status_register"
|
|||
CONF_STATUS_REGISTER_TYPE = "status_register_type"
|
||||
CONF_STEP = "temp_step"
|
||||
CONF_STOPBITS = "stopbits"
|
||||
CONF_SWAP = "swap"
|
||||
CONF_SWAP_BYTE = "byte"
|
||||
CONF_SWAP_NONE = "none"
|
||||
CONF_SWAP_WORD = "word"
|
||||
CONF_SWAP_WORD_BYTE = "word_byte"
|
||||
CONF_SWITCH = "switch"
|
||||
CONF_TARGET_TEMP = "target_temp_register"
|
||||
CONF_VERIFY_REGISTER = "verify_register"
|
||||
|
|
|
@ -43,6 +43,11 @@ from .const import (
|
|||
CONF_REGISTERS,
|
||||
CONF_REVERSE_ORDER,
|
||||
CONF_SCALE,
|
||||
CONF_SWAP,
|
||||
CONF_SWAP_BYTE,
|
||||
CONF_SWAP_NONE,
|
||||
CONF_SWAP_WORD,
|
||||
CONF_SWAP_WORD_BYTE,
|
||||
DATA_TYPE_CUSTOM,
|
||||
DATA_TYPE_FLOAT,
|
||||
DATA_TYPE_INT,
|
||||
|
@ -146,6 +151,28 @@ async def async_setup_platform(
|
|||
)
|
||||
continue
|
||||
|
||||
if CONF_REVERSE_ORDER in entry:
|
||||
if entry[CONF_REVERSE_ORDER]:
|
||||
entry[CONF_SWAP] = CONF_SWAP_WORD
|
||||
else:
|
||||
entry[CONF_SWAP] = CONF_SWAP_NONE
|
||||
del entry[CONF_REVERSE_ORDER]
|
||||
if entry.get(CONF_SWAP) != CONF_SWAP_NONE:
|
||||
if entry[CONF_SWAP] == CONF_SWAP_BYTE:
|
||||
regs_needed = 1
|
||||
else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD
|
||||
regs_needed = 2
|
||||
if (
|
||||
entry[CONF_COUNT] < regs_needed
|
||||
or (entry[CONF_COUNT] % regs_needed) != 0
|
||||
):
|
||||
_LOGGER.error(
|
||||
"Error in sensor %s swap(%s) not possible due to count: %d",
|
||||
entry[CONF_NAME],
|
||||
entry[CONF_SWAP],
|
||||
entry[CONF_COUNT],
|
||||
)
|
||||
continue
|
||||
if CONF_HUB in entry:
|
||||
# from old config!
|
||||
hub: ModbusHub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
|
||||
|
@ -156,20 +183,8 @@ async def async_setup_platform(
|
|||
sensors.append(
|
||||
ModbusRegisterSensor(
|
||||
hub,
|
||||
entry[CONF_NAME],
|
||||
entry.get(CONF_SLAVE),
|
||||
entry[CONF_ADDRESS],
|
||||
entry[CONF_INPUT_TYPE],
|
||||
entry.get(CONF_UNIT_OF_MEASUREMENT),
|
||||
entry[CONF_COUNT],
|
||||
entry[CONF_REVERSE_ORDER],
|
||||
entry[CONF_SCALE],
|
||||
entry[CONF_OFFSET],
|
||||
entry,
|
||||
structure,
|
||||
entry[CONF_PRECISION],
|
||||
entry[CONF_DATA_TYPE],
|
||||
entry.get(CONF_DEVICE_CLASS),
|
||||
entry[CONF_SCAN_INTERVAL],
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -184,39 +199,28 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity):
|
|||
def __init__(
|
||||
self,
|
||||
hub,
|
||||
name,
|
||||
slave,
|
||||
register,
|
||||
register_type,
|
||||
unit_of_measurement,
|
||||
count,
|
||||
reverse_order,
|
||||
scale,
|
||||
offset,
|
||||
entry,
|
||||
structure,
|
||||
precision,
|
||||
data_type,
|
||||
device_class,
|
||||
scan_interval,
|
||||
):
|
||||
"""Initialize the modbus register sensor."""
|
||||
self._hub = hub
|
||||
self._name = name
|
||||
self._name = entry[CONF_NAME]
|
||||
slave = entry.get(CONF_SLAVE)
|
||||
self._slave = int(slave) if slave else None
|
||||
self._register = int(register)
|
||||
self._register_type = register_type
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._count = int(count)
|
||||
self._reverse_order = reverse_order
|
||||
self._scale = scale
|
||||
self._offset = offset
|
||||
self._precision = precision
|
||||
self._register = int(entry[CONF_ADDRESS])
|
||||
self._register_type = entry[CONF_INPUT_TYPE]
|
||||
self._unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
self._count = int(entry[CONF_COUNT])
|
||||
self._swap = entry[CONF_SWAP]
|
||||
self._scale = entry[CONF_SCALE]
|
||||
self._offset = entry[CONF_OFFSET]
|
||||
self._precision = entry[CONF_PRECISION]
|
||||
self._structure = structure
|
||||
self._data_type = data_type
|
||||
self._device_class = device_class
|
||||
self._data_type = entry[CONF_DATA_TYPE]
|
||||
self._device_class = entry.get(CONF_DEVICE_CLASS)
|
||||
self._value = None
|
||||
self._available = True
|
||||
self._scan_interval = timedelta(seconds=scan_interval)
|
||||
self._scan_interval = timedelta(seconds=entry.get(CONF_SCAN_INTERVAL))
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity which will be added."""
|
||||
|
@ -263,6 +267,21 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity):
|
|||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def _swap_registers(self, registers):
|
||||
"""Do swap as needed."""
|
||||
if self._swap in [CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE]:
|
||||
# convert [12][34] --> [21][43]
|
||||
for i, register in enumerate(registers):
|
||||
registers[i] = int.from_bytes(
|
||||
register.to_bytes(2, byteorder="little"),
|
||||
byteorder="big",
|
||||
signed=False,
|
||||
)
|
||||
if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]:
|
||||
# convert [12][34] ==> [34][12]
|
||||
registers.reverse()
|
||||
return registers
|
||||
|
||||
def _update(self):
|
||||
"""Update the state of the sensor."""
|
||||
if self._register_type == CALL_TYPE_REGISTER_INPUT:
|
||||
|
@ -278,10 +297,7 @@ class ModbusRegisterSensor(RestoreEntity, SensorEntity):
|
|||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
registers = result.registers
|
||||
if self._reverse_order:
|
||||
registers.reverse()
|
||||
|
||||
registers = self._swap_registers(result.registers)
|
||||
byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers])
|
||||
if self._data_type == DATA_TYPE_STRING:
|
||||
self._value = byte_string.decode()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""The tests for the Modbus sensor component."""
|
||||
import logging
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
@ -14,6 +15,11 @@ from homeassistant.components.modbus.const import (
|
|||
CONF_REGISTERS,
|
||||
CONF_REVERSE_ORDER,
|
||||
CONF_SCALE,
|
||||
CONF_SWAP,
|
||||
CONF_SWAP_BYTE,
|
||||
CONF_SWAP_NONE,
|
||||
CONF_SWAP_WORD,
|
||||
CONF_SWAP_WORD_BYTE,
|
||||
DATA_TYPE_CUSTOM,
|
||||
DATA_TYPE_FLOAT,
|
||||
DATA_TYPE_INT,
|
||||
|
@ -112,6 +118,38 @@ from .conftest import base_config_test, base_test
|
|||
CONF_DEVICE_CLASS: "battery",
|
||||
},
|
||||
),
|
||||
(
|
||||
True,
|
||||
{
|
||||
CONF_ADDRESS: 51,
|
||||
CONF_COUNT: 1,
|
||||
CONF_SWAP: CONF_SWAP_NONE,
|
||||
},
|
||||
),
|
||||
(
|
||||
True,
|
||||
{
|
||||
CONF_ADDRESS: 51,
|
||||
CONF_COUNT: 1,
|
||||
CONF_SWAP: CONF_SWAP_BYTE,
|
||||
},
|
||||
),
|
||||
(
|
||||
True,
|
||||
{
|
||||
CONF_ADDRESS: 51,
|
||||
CONF_COUNT: 2,
|
||||
CONF_SWAP: CONF_SWAP_WORD,
|
||||
},
|
||||
),
|
||||
(
|
||||
True,
|
||||
{
|
||||
CONF_ADDRESS: 51,
|
||||
CONF_COUNT: 2,
|
||||
CONF_SWAP: CONF_SWAP_WORD_BYTE,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_config_sensor(hass, do_discovery, do_config):
|
||||
|
@ -408,6 +446,51 @@ async def test_config_wrong_struct_sensor(hass, do_config):
|
|||
None,
|
||||
STATE_UNAVAILABLE,
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_COUNT: 1,
|
||||
CONF_DATA_TYPE: DATA_TYPE_INT,
|
||||
CONF_SWAP: CONF_SWAP_NONE,
|
||||
},
|
||||
[0x0102],
|
||||
str(int(0x0102)),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_COUNT: 1,
|
||||
CONF_DATA_TYPE: DATA_TYPE_INT,
|
||||
CONF_SWAP: CONF_SWAP_BYTE,
|
||||
},
|
||||
[0x0201],
|
||||
str(int(0x0102)),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_COUNT: 2,
|
||||
CONF_DATA_TYPE: DATA_TYPE_INT,
|
||||
CONF_SWAP: CONF_SWAP_BYTE,
|
||||
},
|
||||
[0x0102, 0x0304],
|
||||
str(int(0x02010403)),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_COUNT: 2,
|
||||
CONF_DATA_TYPE: DATA_TYPE_INT,
|
||||
CONF_SWAP: CONF_SWAP_WORD,
|
||||
},
|
||||
[0x0102, 0x0304],
|
||||
str(int(0x03040102)),
|
||||
),
|
||||
(
|
||||
{
|
||||
CONF_COUNT: 2,
|
||||
CONF_DATA_TYPE: DATA_TYPE_INT,
|
||||
CONF_SWAP: CONF_SWAP_WORD_BYTE,
|
||||
},
|
||||
[0x0102, 0x0304],
|
||||
str(int(0x04030201)),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_all_sensor(hass, cfg, regs, expected):
|
||||
|
@ -508,3 +591,33 @@ async def test_restore_state_sensor(hass):
|
|||
)
|
||||
entity_id = f"{SENSOR_DOMAIN}.{sensor_name}"
|
||||
assert hass.states.get(entity_id).state == test_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"swap_type",
|
||||
[CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE],
|
||||
)
|
||||
async def test_swap_sensor_wrong_config(hass, caplog, swap_type):
|
||||
"""Run test for sensor swap."""
|
||||
sensor_name = "modbus_test_sensor"
|
||||
config = {
|
||||
CONF_NAME: sensor_name,
|
||||
CONF_ADDRESS: 1234,
|
||||
CONF_COUNT: 1,
|
||||
CONF_SWAP: swap_type,
|
||||
CONF_DATA_TYPE: DATA_TYPE_INT,
|
||||
}
|
||||
|
||||
caplog.set_level(logging.ERROR)
|
||||
caplog.clear()
|
||||
await base_config_test(
|
||||
hass,
|
||||
config,
|
||||
sensor_name,
|
||||
SENSOR_DOMAIN,
|
||||
CONF_SENSORS,
|
||||
None,
|
||||
method_discovery=True,
|
||||
expect_init_to_fail=True,
|
||||
)
|
||||
assert caplog.messages[-1].startswith("Error in sensor " + sensor_name + " swap")
|
||||
|
|
Loading…
Reference in New Issue