diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 2defb32393d..35abdca48fe 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -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, diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index ffe89757ef1..f5c7dced77d 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -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" diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index c747d0a29d0..81b54cb62e1 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -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() diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index fb8a00f8c07..b8ab10953c8 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -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")