mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 08:47:09 +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_OFF_VALUE,
|
||||
CONF_HVAC_ON_VALUE,
|
||||
CONF_HVAC_ONOFF_COIL,
|
||||
CONF_HVAC_ONOFF_REGISTER,
|
||||
CONF_INPUT_TYPE,
|
||||
CONF_MAX_TEMP,
|
||||
@ -258,7 +259,8 @@ CLIMATE_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_MIN_TEMP, default=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_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(
|
||||
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
|
||||
): cv.positive_int,
|
||||
|
@ -43,7 +43,9 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import get_hub
|
||||
from .const import (
|
||||
CALL_TYPE_COIL,
|
||||
CALL_TYPE_REGISTER_HOLDING,
|
||||
CALL_TYPE_WRITE_COIL,
|
||||
CALL_TYPE_WRITE_REGISTER,
|
||||
CALL_TYPE_WRITE_REGISTERS,
|
||||
CONF_CLIMATES,
|
||||
@ -70,6 +72,7 @@ from .const import (
|
||||
CONF_HVAC_MODE_VALUES,
|
||||
CONF_HVAC_OFF_VALUE,
|
||||
CONF_HVAC_ON_VALUE,
|
||||
CONF_HVAC_ONOFF_COIL,
|
||||
CONF_HVAC_ONOFF_REGISTER,
|
||||
CONF_MAX_TEMP,
|
||||
CONF_MIN_TEMP,
|
||||
@ -254,6 +257,13 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
||||
else:
|
||||
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:
|
||||
"""Handle entity which will be added."""
|
||||
await self.async_base_added_to_hass()
|
||||
@ -287,6 +297,15 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
||||
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:
|
||||
# Write a value to the mode register for the desired mode.
|
||||
for value, mode in self._hvac_mode_mapping:
|
||||
@ -484,6 +503,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
||||
if onoff == self._hvac_off_value:
|
||||
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(
|
||||
self, register_type: str, register: int, raw: bool | None = False
|
||||
) -> float | None:
|
||||
@ -508,3 +532,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
||||
return None
|
||||
self._attr_available = True
|
||||
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_ON_VALUE = "hvac_on_value"
|
||||
CONF_HVAC_OFF_VALUE = "hvac_off_value"
|
||||
CONF_HVAC_ONOFF_COIL = "hvac_onoff_coil"
|
||||
CONF_HVAC_MODE_OFF = "state_off"
|
||||
CONF_HVAC_MODE_HEAT = "state_heat"
|
||||
CONF_HVAC_MODE_COOL = "state_cool"
|
||||
|
@ -58,6 +58,7 @@ from homeassistant.components.modbus.const import (
|
||||
CONF_HVAC_MODE_VALUES,
|
||||
CONF_HVAC_OFF_VALUE,
|
||||
CONF_HVAC_ON_VALUE,
|
||||
CONF_HVAC_ONOFF_COIL,
|
||||
CONF_HVAC_ONOFF_REGISTER,
|
||||
CONF_MAX_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]
|
||||
|
||||
|
||||
@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(
|
||||
"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)
|
||||
|
||||
|
||||
@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(
|
||||
"do_config",
|
||||
[
|
||||
@ -562,6 +625,126 @@ async def test_service_climate_update(
|
||||
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(
|
||||
("do_config", "result", "register_words"),
|
||||
[
|
||||
|
Loading…
x
Reference in New Issue
Block a user