mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 01:07:10 +00:00
Enable Modbus Climate / HVAC on/off to use the coil instead of the register(s) (#135657)
This commit is contained in:
parent
d3da3b3470
commit
4cab773bab
@ -90,6 +90,7 @@ from .const import (
|
|||||||
CONF_HVAC_MODE_VALUES,
|
CONF_HVAC_MODE_VALUES,
|
||||||
CONF_HVAC_OFF_VALUE,
|
CONF_HVAC_OFF_VALUE,
|
||||||
CONF_HVAC_ON_VALUE,
|
CONF_HVAC_ON_VALUE,
|
||||||
|
CONF_HVAC_ONOFF_COIL,
|
||||||
CONF_HVAC_ONOFF_REGISTER,
|
CONF_HVAC_ONOFF_REGISTER,
|
||||||
CONF_INPUT_TYPE,
|
CONF_INPUT_TYPE,
|
||||||
CONF_MAX_TEMP,
|
CONF_MAX_TEMP,
|
||||||
@ -258,7 +259,8 @@ CLIMATE_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
|
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
|
||||||
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
|
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
|
||||||
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
|
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
|
||||||
vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int,
|
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(
|
vol.Optional(
|
||||||
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
|
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
|
||||||
): cv.positive_int,
|
): cv.positive_int,
|
||||||
|
@ -43,7 +43,9 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||||||
|
|
||||||
from . import get_hub
|
from . import get_hub
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CALL_TYPE_COIL,
|
||||||
CALL_TYPE_REGISTER_HOLDING,
|
CALL_TYPE_REGISTER_HOLDING,
|
||||||
|
CALL_TYPE_WRITE_COIL,
|
||||||
CALL_TYPE_WRITE_REGISTER,
|
CALL_TYPE_WRITE_REGISTER,
|
||||||
CALL_TYPE_WRITE_REGISTERS,
|
CALL_TYPE_WRITE_REGISTERS,
|
||||||
CONF_CLIMATES,
|
CONF_CLIMATES,
|
||||||
@ -70,6 +72,7 @@ from .const import (
|
|||||||
CONF_HVAC_MODE_VALUES,
|
CONF_HVAC_MODE_VALUES,
|
||||||
CONF_HVAC_OFF_VALUE,
|
CONF_HVAC_OFF_VALUE,
|
||||||
CONF_HVAC_ON_VALUE,
|
CONF_HVAC_ON_VALUE,
|
||||||
|
CONF_HVAC_ONOFF_COIL,
|
||||||
CONF_HVAC_ONOFF_REGISTER,
|
CONF_HVAC_ONOFF_REGISTER,
|
||||||
CONF_MAX_TEMP,
|
CONF_MAX_TEMP,
|
||||||
CONF_MIN_TEMP,
|
CONF_MIN_TEMP,
|
||||||
@ -254,6 +257,13 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
else:
|
else:
|
||||||
self._hvac_onoff_register = None
|
self._hvac_onoff_register = None
|
||||||
|
|
||||||
|
if CONF_HVAC_ONOFF_COIL in config:
|
||||||
|
self._hvac_onoff_coil = config[CONF_HVAC_ONOFF_COIL]
|
||||||
|
if HVACMode.OFF not in self._attr_hvac_modes:
|
||||||
|
self._attr_hvac_modes.append(HVACMode.OFF)
|
||||||
|
else:
|
||||||
|
self._hvac_onoff_coil = None
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle entity which will be added."""
|
"""Handle entity which will be added."""
|
||||||
await self.async_base_added_to_hass()
|
await self.async_base_added_to_hass()
|
||||||
@ -287,6 +297,15 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
CALL_TYPE_WRITE_REGISTER,
|
CALL_TYPE_WRITE_REGISTER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._hvac_onoff_coil is not None:
|
||||||
|
# Turn HVAC Off by writing 0 to the On/Off coil, or 1 otherwise.
|
||||||
|
await self._hub.async_pb_call(
|
||||||
|
self._slave,
|
||||||
|
self._hvac_onoff_coil,
|
||||||
|
0 if hvac_mode == HVACMode.OFF else 1,
|
||||||
|
CALL_TYPE_WRITE_COIL,
|
||||||
|
)
|
||||||
|
|
||||||
if self._hvac_mode_register is not None:
|
if self._hvac_mode_register is not None:
|
||||||
# Write a value to the mode register for the desired mode.
|
# Write a value to the mode register for the desired mode.
|
||||||
for value, mode in self._hvac_mode_mapping:
|
for value, mode in self._hvac_mode_mapping:
|
||||||
@ -484,6 +503,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
if onoff == self._hvac_off_value:
|
if onoff == self._hvac_off_value:
|
||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
|
if self._hvac_onoff_coil is not None:
|
||||||
|
onoff = await self._async_read_coil(self._hvac_onoff_coil)
|
||||||
|
if onoff == 0:
|
||||||
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
async def _async_read_register(
|
async def _async_read_register(
|
||||||
self, register_type: str, register: int, raw: bool | None = False
|
self, register_type: str, register: int, raw: bool | None = False
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@ -508,3 +532,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
return None
|
return None
|
||||||
self._attr_available = True
|
self._attr_available = True
|
||||||
return float(self._value)
|
return float(self._value)
|
||||||
|
|
||||||
|
async def _async_read_coil(self, address: int) -> int | None:
|
||||||
|
result = await self._hub.async_pb_call(self._slave, address, 1, CALL_TYPE_COIL)
|
||||||
|
if result is not None and result.bits is not None:
|
||||||
|
self._attr_available = True
|
||||||
|
return int(result.bits[0])
|
||||||
|
self._attr_available = False
|
||||||
|
return None
|
||||||
|
@ -62,6 +62,7 @@ CONF_HVAC_MODE_REGISTER = "hvac_mode_register"
|
|||||||
CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register"
|
CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register"
|
||||||
CONF_HVAC_ON_VALUE = "hvac_on_value"
|
CONF_HVAC_ON_VALUE = "hvac_on_value"
|
||||||
CONF_HVAC_OFF_VALUE = "hvac_off_value"
|
CONF_HVAC_OFF_VALUE = "hvac_off_value"
|
||||||
|
CONF_HVAC_ONOFF_COIL = "hvac_onoff_coil"
|
||||||
CONF_HVAC_MODE_OFF = "state_off"
|
CONF_HVAC_MODE_OFF = "state_off"
|
||||||
CONF_HVAC_MODE_HEAT = "state_heat"
|
CONF_HVAC_MODE_HEAT = "state_heat"
|
||||||
CONF_HVAC_MODE_COOL = "state_cool"
|
CONF_HVAC_MODE_COOL = "state_cool"
|
||||||
|
@ -58,6 +58,7 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_HVAC_MODE_VALUES,
|
CONF_HVAC_MODE_VALUES,
|
||||||
CONF_HVAC_OFF_VALUE,
|
CONF_HVAC_OFF_VALUE,
|
||||||
CONF_HVAC_ON_VALUE,
|
CONF_HVAC_ON_VALUE,
|
||||||
|
CONF_HVAC_ONOFF_COIL,
|
||||||
CONF_HVAC_ONOFF_REGISTER,
|
CONF_HVAC_ONOFF_REGISTER,
|
||||||
CONF_MAX_TEMP,
|
CONF_MAX_TEMP,
|
||||||
CONF_MIN_TEMP,
|
CONF_MIN_TEMP,
|
||||||
@ -366,6 +367,29 @@ async def test_config_hvac_onoff_register(hass: HomeAssistant, mock_modbus) -> N
|
|||||||
assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES]
|
assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"do_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 117,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_HVAC_ONOFF_REGISTER: 11,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_config_hvac_onoff_coil(hass: HomeAssistant, mock_modbus) -> None:
|
||||||
|
"""Run configuration test for On/Off coil."""
|
||||||
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
assert HVACMode.OFF in state.attributes[ATTR_HVAC_MODES]
|
||||||
|
assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"do_config",
|
"do_config",
|
||||||
[
|
[
|
||||||
@ -407,6 +431,45 @@ async def test_hvac_onoff_values(hass: HomeAssistant, mock_modbus) -> None:
|
|||||||
mock_modbus.write_register.assert_called_with(11, value=0xFF, slave=10)
|
mock_modbus.write_register.assert_called_with(11, value=0xFF, slave=10)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"do_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 117,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_HVAC_ONOFF_COIL: 11,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_hvac_onoff_coil(hass: HomeAssistant, mock_modbus) -> None:
|
||||||
|
"""Run configuration test for On/Off coil values."""
|
||||||
|
await hass.services.async_call(
|
||||||
|
CLIMATE_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_modbus.write_coil.assert_called_with(11, value=1, slave=10)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
CLIMATE_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_modbus.write_coil.assert_called_with(11, value=0, slave=10)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"do_config",
|
"do_config",
|
||||||
[
|
[
|
||||||
@ -562,6 +625,126 @@ async def test_service_climate_update(
|
|||||||
assert hass.states.get(ENTITY_ID).state == result
|
assert hass.states.get(ENTITY_ID).state == result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("do_config", "result", "register_words", "coil_value"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: [130, 131, 132, 133, 134, 135, 136],
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_MODE_REGISTER: {
|
||||||
|
CONF_ADDRESS: 118,
|
||||||
|
CONF_HVAC_MODE_VALUES: {
|
||||||
|
CONF_HVAC_MODE_COOL: 0,
|
||||||
|
CONF_HVAC_MODE_HEAT: 1,
|
||||||
|
CONF_HVAC_MODE_DRY: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CONF_HVAC_ONOFF_COIL: 11,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
HVACMode.COOL,
|
||||||
|
[0x00],
|
||||||
|
[0x01],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 119,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_MODE_REGISTER: {
|
||||||
|
CONF_ADDRESS: 118,
|
||||||
|
CONF_HVAC_MODE_VALUES: {
|
||||||
|
CONF_HVAC_MODE_COOL: 0,
|
||||||
|
CONF_HVAC_MODE_HEAT: 1,
|
||||||
|
CONF_HVAC_MODE_DRY: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CONF_HVAC_ONOFF_COIL: 11,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
HVACMode.HEAT,
|
||||||
|
[0x01],
|
||||||
|
[0x01],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 120,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_MODE_REGISTER: {
|
||||||
|
CONF_ADDRESS: 118,
|
||||||
|
CONF_HVAC_MODE_VALUES: {
|
||||||
|
CONF_HVAC_MODE_COOL: 0,
|
||||||
|
CONF_HVAC_MODE_HEAT: 2,
|
||||||
|
CONF_HVAC_MODE_DRY: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CONF_HVAC_ONOFF_COIL: 11,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
HVACMode.OFF,
|
||||||
|
[0x00],
|
||||||
|
[0x00],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 120,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_ONOFF_COIL: 11,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unavailable",
|
||||||
|
[0x00],
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_hvac_onoff_coil_update(
|
||||||
|
hass: HomeAssistant, mock_modbus_ha, result, register_words, coil_value
|
||||||
|
) -> None:
|
||||||
|
"""Test climate update based on On/Off coil values."""
|
||||||
|
mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words)
|
||||||
|
mock_modbus_ha.read_coils.return_value = ReadResult(coil_value)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
HOMEASSISTANT_DOMAIN,
|
||||||
|
SERVICE_UPDATE_ENTITY,
|
||||||
|
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
assert state.state == result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("do_config", "result", "register_words"),
|
("do_config", "result", "register_words"),
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user