Add swap byte/word/byteword option to modbus sensor (#49719)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/49744/head
jan iversen 2021-04-27 10:49:41 +02:00 committed by GitHub
parent 1b957a0ce0
commit e5e215353d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 43 deletions

View File

@ -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,

View File

@ -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"

View File

@ -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()

View File

@ -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")