Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
288bb62
Support of current temp scale and offset
illia-piskurov Aug 21, 2025
3d222d0
Merge branch 'dev' into curr_temp_scale_and_offset
janiversen Aug 21, 2025
6feb103
Try another way
illia-piskurov Aug 22, 2025
5061afb
Separated offset and scale
illia-piskurov Sep 1, 2025
ab50e43
Removed mistakes
illia-piskurov Sep 2, 2025
29b3202
Change in sensor.py
illia-piskurov Sep 2, 2025
2fd3da3
Merge branch 'home-assistant:dev' into curr_temp_scale_and_offset
crug80 Sep 5, 2025
818221d
rev2-crug
crug80 Sep 5, 2025
148002c
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Sep 5, 2025
57af459
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Sep 8, 2025
ad3da15
Merge branch 'dev' into curr_temp_scale_and_offset
janiversen Sep 8, 2025
33fec8d
floats
crug80 Sep 8, 2025
a8da718
fix requests
crug80 Sep 9, 2025
cf5662d
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Sep 9, 2025
0cd6f6a
fix requests 2
crug80 Sep 9, 2025
e45ce85
Merge branch 'home-assistant:dev' into curr_temp_scale_and_offset
crug80 Sep 12, 2025
91366b5
Merge branch 'dev' into curr_temp_scale_and_offset
janiversen Sep 13, 2025
c9ae098
fix requests 3
crug80 Sep 13, 2025
30ef8e5
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Sep 16, 2025
cab1082
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Sep 22, 2025
2b650a2
fix ruff
crug80 Sep 22, 2025
010f556
Merge branch 'dev' into curr_temp_scale_and_offset
illia-piskurov Sep 24, 2025
82a53ed
Merge branch 'dev' into curr_temp_scale_and_offset
illia-piskurov Sep 30, 2025
7188f6c
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Oct 8, 2025
de33a03
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Oct 19, 2025
f7d30ef
Merge branch 'dev' into curr_temp_scale_and_offset
frenck Oct 19, 2025
bea9139
Update homeassistant/components/modbus/validators.py
crug80 Oct 20, 2025
ae53aef
Merge branch 'dev' into curr_temp_scale_and_offset
illia-piskurov Oct 30, 2025
9032dbf
Merge branch 'home-assistant:dev' into curr_temp_scale_and_offset
crug80 Oct 30, 2025
3496c7c
fix emontnemery request
crug80 Oct 30, 2025
c56ef49
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Oct 30, 2025
d60deca
Merge branch 'dev' into curr_temp_scale_and_offset
crug80 Oct 31, 2025
4153de7
Merge branch 'dev' into curr_temp_scale_and_offset
emontnemery Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions homeassistant/components/modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
CONF_BYTESIZE,
CONF_CLIMATES,
CONF_COLOR_TEMP_REGISTER,
CONF_CURRENT_TEMP_OFFSET,
CONF_CURRENT_TEMP_SCALE,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_AUTO,
Expand Down Expand Up @@ -137,6 +139,8 @@
CONF_SWING_MODE_SWING_VERT,
CONF_SWING_MODE_VALUES,
CONF_TARGET_TEMP,
CONF_TARGET_TEMP_OFFSET,
CONF_TARGET_TEMP_SCALE,
CONF_TARGET_TEMP_WRITE_REGISTERS,
CONF_VERIFY,
CONF_VIRTUAL_COUNT,
Expand All @@ -159,8 +163,10 @@
from .validators import (
duplicate_fan_mode_validator,
duplicate_swing_mode_validator,
ensure_and_check_conflicting_scales_and_offsets,
hvac_fixedsize_reglist_validator,
nan_validator,
not_zero_value,
register_int_list_validator,
struct_validator,
)
Expand Down Expand Up @@ -210,8 +216,10 @@
]
),
vol.Optional(CONF_STRUCTURE): cv.string,
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
vol.Optional(CONF_SCALE): vol.All(
vol.Coerce(float), lambda v: not_zero_value(v, "Scale cannot be zero.")
),
vol.Optional(CONF_OFFSET): vol.Coerce(float),
vol.Optional(CONF_PRECISION): cv.positive_int,
vol.Optional(
CONF_SWAP,
Expand Down Expand Up @@ -273,6 +281,18 @@
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int,
vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int,
vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.All(
vol.Coerce(float),
lambda v: not_zero_value(
v, "Current temperature scale cannot be zero."
),
),
vol.Optional(CONF_TARGET_TEMP_SCALE): vol.All(
vol.Coerce(float),
lambda v: not_zero_value(v, "Target temperature scale cannot be zero."),
),
vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float),
vol.Optional(
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
): cv.positive_int,
Expand Down Expand Up @@ -385,6 +405,7 @@
),
},
),
ensure_and_check_conflicting_scales_and_offsets,
)

COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
Expand Down
53 changes: 45 additions & 8 deletions homeassistant/components/modbus/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
CALL_TYPE_WRITE_REGISTER,
CALL_TYPE_WRITE_REGISTERS,
CONF_CLIMATES,
CONF_CURRENT_TEMP_OFFSET,
CONF_CURRENT_TEMP_SCALE,
CONF_FAN_MODE_AUTO,
CONF_FAN_MODE_DIFFUSE,
CONF_FAN_MODE_FOCUS,
Expand Down Expand Up @@ -97,8 +99,12 @@
CONF_SWING_MODE_SWING_VERT,
CONF_SWING_MODE_VALUES,
CONF_TARGET_TEMP,
CONF_TARGET_TEMP_OFFSET,
CONF_TARGET_TEMP_SCALE,
CONF_TARGET_TEMP_WRITE_REGISTERS,
CONF_WRITE_REGISTERS,
DEFAULT_OFFSET,
DEFAULT_SCALE,
DataType,
)
from .entity import ModbusStructEntity
Expand Down Expand Up @@ -166,6 +172,10 @@ def __init__(
self._attr_min_temp = config[CONF_MIN_TEMP]
self._attr_max_temp = config[CONF_MAX_TEMP]
self._attr_target_temperature_step = config[CONF_STEP]
self._current_temp_scale = config[CONF_CURRENT_TEMP_SCALE]
self._current_temp_offset = config[CONF_CURRENT_TEMP_OFFSET]
self._target_temp_scale = config[CONF_TARGET_TEMP_SCALE]
self._target_temp_offset = config[CONF_TARGET_TEMP_OFFSET]

if CONF_HVAC_MODE_REGISTER in config:
mode_config = config[CONF_HVAC_MODE_REGISTER]
Expand Down Expand Up @@ -413,8 +423,8 @@ async def async_set_swing_mode(self, swing_mode: str) -> None:
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_temperature = (
float(kwargs[ATTR_TEMPERATURE]) - self._offset
) / self._scale
float(kwargs[ATTR_TEMPERATURE]) - self._target_temp_offset
) / self._target_temp_scale
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you validate for target/current scale being configured to 0.

It does not make sense, but it is possible.

if self._data_type in (
DataType.INT16,
DataType.INT32,
Expand Down Expand Up @@ -472,15 +482,25 @@ async def _async_update(self) -> None:
self._target_temperature_register[
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
],
self._target_temp_scale,
self._target_temp_offset,
)

self._attr_current_temperature = await self._async_read_register(
self._input_type, self._address
self._input_type,
self._address,
self._current_temp_scale,
self._current_temp_offset,
)

# Read the HVAC mode register if defined
if self._hvac_mode_register is not None:
hvac_mode = await self._async_read_register(
CALL_TYPE_REGISTER_HOLDING, self._hvac_mode_register, raw=True
CALL_TYPE_REGISTER_HOLDING,
self._hvac_mode_register,
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)

# Translate the value received
Expand All @@ -499,7 +519,11 @@ async def _async_update(self) -> None:
# Read the HVAC action register if defined
if self._hvac_action_register is not None:
hvac_action = await self._async_read_register(
self._hvac_action_type, self._hvac_action_register, raw=True
self._hvac_action_type,
self._hvac_action_register,
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)

# Translate the value received
Expand All @@ -517,6 +541,8 @@ async def _async_update(self) -> None:
self._fan_mode_register
if isinstance(self._fan_mode_register, int)
else self._fan_mode_register[0],
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)

Expand All @@ -533,6 +559,8 @@ async def _async_update(self) -> None:
self._swing_mode_register
if isinstance(self._swing_mode_register, int)
else self._swing_mode_register[0],
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)

Expand All @@ -551,7 +579,11 @@ async def _async_update(self) -> None:
# in the mode register.
if self._hvac_onoff_register is not None:
onoff = await self._async_read_register(
CALL_TYPE_REGISTER_HOLDING, self._hvac_onoff_register, raw=True
CALL_TYPE_REGISTER_HOLDING,
self._hvac_onoff_register,
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)
if onoff == self._hvac_off_value:
self._attr_hvac_mode = HVACMode.OFF
Expand All @@ -562,7 +594,12 @@ async def _async_update(self) -> None:
self._attr_hvac_mode = HVACMode.OFF

async def _async_read_register(
self, register_type: str, register: int, raw: bool | None = False
self,
register_type: str,
register: int,
scale: float,
offset: float,
raw: bool | None = False,
) -> float | None:
"""Read register using the Modbus hub slave."""
result = await self._hub.async_pb_call(
Expand All @@ -579,7 +616,7 @@ async def _async_read_register(
return int(result.registers[0])

# The regular handling of the value
self._value = self.unpack_structure_result(result.registers)
self._value = self.unpack_structure_result(result.registers, scale, offset)
if not self._value:
self._attr_available = False
return None
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/modbus/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
CONF_CLIMATES = "climates"
CONF_BRIGHTNESS_REGISTER = "brightness_address"
CONF_COLOR_TEMP_REGISTER = "color_temp_address"
CONF_CURRENT_TEMP_OFFSET = "current_temp_offset"
CONF_CURRENT_TEMP_SCALE = "current_temp_scale"
CONF_DATA_TYPE = "data_type"
CONF_DEVICE_ADDRESS = "device_address"
CONF_FANS = "fans"
Expand Down Expand Up @@ -48,6 +50,8 @@
CONF_SWAP_WORD = "word"
CONF_SWAP_WORD_BYTE = "word_byte"
CONF_TARGET_TEMP = "target_temp_register"
CONF_TARGET_TEMP_OFFSET = "target_temp_offset"
CONF_TARGET_TEMP_SCALE = "target_temp_scale"
CONF_TARGET_TEMP_WRITE_REGISTERS = "target_temp_write_registers"
CONF_FAN_MODE_REGISTER = "fan_mode_register"
CONF_FAN_MODE_ON = "state_fan_on"
Expand Down Expand Up @@ -181,4 +185,7 @@ class DataType(str, Enum):
LIGHT_MODBUS_SCALE_MAX = 100
LIGHT_MODBUS_INVALID_VALUE = 0xFFFF

DEFAULT_SCALE = 1.0
DEFAULT_OFFSET = 0

_LOGGER = logging.getLogger(__package__)
28 changes: 17 additions & 11 deletions homeassistant/components/modbus/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
CONF_DELAY,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_OFFSET,
CONF_SCAN_INTERVAL,
CONF_SLAVE,
CONF_STRUCTURE,
Expand Down Expand Up @@ -50,7 +49,6 @@
CONF_MIN_VALUE,
CONF_NAN_VALUE,
CONF_PRECISION,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_OFF,
CONF_STATE_ON,
Expand All @@ -62,6 +60,8 @@
CONF_VIRTUAL_COUNT,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
DEFAULT_OFFSET,
DEFAULT_SCALE,
SIGNAL_STOP_ENTITY,
DataType,
)
Expand Down Expand Up @@ -163,8 +163,6 @@ def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None:
self._swap = config[CONF_SWAP]
self._data_type = config[CONF_DATA_TYPE]
self._structure: str = config[CONF_STRUCTURE]
self._scale = config[CONF_SCALE]
self._offset = config[CONF_OFFSET]
self._slave_count = config.get(CONF_SLAVE_COUNT) or config.get(
CONF_VIRTUAL_COUNT, 0
)
Expand All @@ -181,8 +179,6 @@ def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None:
self._precision = config.get(CONF_PRECISION, 2)
else:
self._precision = config.get(CONF_PRECISION, 0)
if self._precision > 0 or self._scale != int(self._scale):
self._value_is_int = False
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing precision here, might be correct, but it seems it is not added to climate ??

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Climate never used self._value_is_int, so I moved that inside the sensor only,m where _value_is_int is used.
If _value_is_int should be used also by a climate, we should open a bug, because actually it doesn't..


def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
"""Do swap as needed."""
Expand All @@ -206,7 +202,12 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
registers.reverse()
return registers

def __process_raw_value(self, entry: float | str | bytes) -> str | None:
def __process_raw_value(
self,
entry: float | bytes,
scale: float = DEFAULT_SCALE,
offset: float = DEFAULT_OFFSET,
) -> str | None:
Comment thread
illia-piskurov marked this conversation as resolved.
"""Process value from sensor with NaN handling, scaling, offset, min/max etc."""
if self._nan_value is not None and entry in (self._nan_value, -self._nan_value):
return None
Expand All @@ -215,7 +216,7 @@ def __process_raw_value(self, entry: float | str | bytes) -> str | None:
if entry != entry: # noqa: PLR0124
# NaN float detection replace with None
return None
val: float | int = self._scale * entry + self._offset
val: float | int = scale * entry + offset
if self._min_value is not None and val < self._min_value:
val = self._min_value
if self._max_value is not None and val > self._max_value:
Expand All @@ -226,7 +227,12 @@ def __process_raw_value(self, entry: float | str | bytes) -> str | None:
return str(round(val))
return f"{float(val):.{self._precision}f}"

def unpack_structure_result(self, registers: list[int]) -> str | None:
def unpack_structure_result(
self,
registers: list[int],
scale: float = DEFAULT_SCALE,
offset: float = DEFAULT_OFFSET,
) -> str | None:
"""Convert registers to proper result."""

if self._swap:
Expand All @@ -250,15 +256,15 @@ def unpack_structure_result(self, registers: list[int]) -> str | None:
# Apply scale, precision, limits to floats and ints
v_result = []
for entry in val:
v_temp = self.__process_raw_value(entry)
v_temp = self.__process_raw_value(entry, scale, offset)
if self._data_type != DataType.CUSTOM:
v_result.append(str(v_temp))
else:
v_result.append(str(v_temp) if v_temp is not None else "0")
return ",".join(map(str, v_result))

# Apply scale, precision, limits to floats and ints
return self.__process_raw_value(val[0])
return self.__process_raw_value(val[0], scale, offset)


class ModbusToggleEntity(ModbusBaseEntity, ToggleEntity, RestoreEntity):
Expand Down
18 changes: 16 additions & 2 deletions homeassistant/components/modbus/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_OFFSET,
CONF_SENSORS,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
Expand All @@ -25,7 +26,14 @@
)

from . import get_hub
from .const import _LOGGER, CONF_SLAVE_COUNT, CONF_VIRTUAL_COUNT
from .const import (
_LOGGER,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_VIRTUAL_COUNT,
DEFAULT_OFFSET,
DEFAULT_SCALE,
)
from .entity import ModbusStructEntity
from .modbus import ModbusHub

Expand Down Expand Up @@ -73,9 +81,13 @@ def __init__(
self._coordinator: DataUpdateCoordinator[list[float | None] | None] | None = (
None
)
self._scale = entry.get(CONF_SCALE, DEFAULT_SCALE)
self._offset = entry.get(CONF_OFFSET, DEFAULT_OFFSET)
self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
self._attr_state_class = entry.get(CONF_STATE_CLASS)
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
if self._precision > 0 or self._scale != int(self._scale):
self._value_is_int = False

async def async_setup_slaves(
self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any]
Expand Down Expand Up @@ -117,7 +129,9 @@ async def _async_update(self) -> None:
self.async_write_ha_state()
return
self._attr_available = True
result = self.unpack_structure_result(raw_result.registers)
result = self.unpack_structure_result(
raw_result.registers, self._scale, self._offset
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpack_structure_result types scale/offset as floats, but _scale/_offset are typed float | int !

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still a problem.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??

)
if self._coordinator:
result_array: list[float | None] = []
if result:
Expand Down
Loading
Loading