Add set_profile service for Vallox integration (#120225)

* Add set_profile service for Vallox integration

* Merge profile constants, use str input for service

* add service test and some related refactoring

* Change service uom to 'minutes'

Co-authored-by: Sebastian Lövdahl <slovdahl@hibox.fi>

* Update icons.js format after rebase

* Translate profile names for service

* Fix test using wrong dict

---------

Co-authored-by: Sebastian Lövdahl <slovdahl@hibox.fi>
This commit is contained in:
treetip 2024-09-08 16:07:42 +03:00 committed by GitHub
parent 45ab6e9b06
commit af62e8267f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 146 additions and 17 deletions

View file

@ -22,6 +22,7 @@ from .const import (
DEFAULT_FAN_SPEED_HOME,
DEFAULT_NAME,
DOMAIN,
I18N_KEY_TO_VALLOX_PROFILE,
)
from .coordinator import ValloxDataUpdateCoordinator
@ -61,6 +62,18 @@ SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED = vol.Schema(
}
)
ATTR_PROFILE = "profile"
ATTR_DURATION = "duration"
SERVICE_SCHEMA_SET_PROFILE = vol.Schema(
{
vol.Required(ATTR_PROFILE): vol.In(I18N_KEY_TO_VALLOX_PROFILE),
vol.Optional(ATTR_DURATION): vol.All(
vol.Coerce(int), vol.Clamp(min=1, max=65535)
),
}
)
class ServiceMethodDetails(NamedTuple):
"""Details for SERVICE_TO_METHOD mapping."""
@ -72,6 +85,7 @@ class ServiceMethodDetails(NamedTuple):
SERVICE_SET_PROFILE_FAN_SPEED_HOME = "set_profile_fan_speed_home"
SERVICE_SET_PROFILE_FAN_SPEED_AWAY = "set_profile_fan_speed_away"
SERVICE_SET_PROFILE_FAN_SPEED_BOOST = "set_profile_fan_speed_boost"
SERVICE_SET_PROFILE = "set_profile"
SERVICE_TO_METHOD = {
SERVICE_SET_PROFILE_FAN_SPEED_HOME: ServiceMethodDetails(
@ -86,6 +100,9 @@ SERVICE_TO_METHOD = {
method="async_set_profile_fan_speed_boost",
schema=SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED,
),
SERVICE_SET_PROFILE: ServiceMethodDetails(
method="async_set_profile", schema=SERVICE_SCHEMA_SET_PROFILE
),
}
@ -183,6 +200,22 @@ class ValloxServiceHandler:
return False
return True
async def async_set_profile(
self, profile: str, duration: int | None = None
) -> bool:
"""Activate profile for given duration."""
_LOGGER.debug("Activating profile %s for %s min", profile, duration)
try:
await self._client.set_profile(
I18N_KEY_TO_VALLOX_PROFILE[profile], duration
)
except ValloxApiException as err:
_LOGGER.error(
"Error setting profile %d for duration %s: %s", profile, duration, err
)
return False
return True
async def async_handle(self, call: ServiceCall) -> None:
"""Dispatch a service call."""
service_details = SERVICE_TO_METHOD.get(call.service)

View file

@ -22,14 +22,15 @@ DEFAULT_FAN_SPEED_HOME = 50
DEFAULT_FAN_SPEED_AWAY = 25
DEFAULT_FAN_SPEED_BOOST = 65
VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE = {
VALLOX_PROFILE.HOME: "Home",
VALLOX_PROFILE.AWAY: "Away",
VALLOX_PROFILE.BOOST: "Boost",
VALLOX_PROFILE.FIREPLACE: "Fireplace",
I18N_KEY_TO_VALLOX_PROFILE = {
"home": VALLOX_PROFILE.HOME,
"away": VALLOX_PROFILE.AWAY,
"boost": VALLOX_PROFILE.BOOST,
"fireplace": VALLOX_PROFILE.FIREPLACE,
"extra": VALLOX_PROFILE.EXTRA,
}
VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE = {
VALLOX_PROFILE_TO_PRESET_MODE = {
VALLOX_PROFILE.HOME: "Home",
VALLOX_PROFILE.AWAY: "Away",
VALLOX_PROFILE.BOOST: "Boost",
@ -37,8 +38,8 @@ VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE = {
VALLOX_PROFILE.EXTRA: "Extra",
}
PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE = {
value: key for (key, value) in VALLOX_PROFILE_TO_PRESET_MODE_SETTABLE.items()
PRESET_MODE_TO_VALLOX_PROFILE = {
value: key for (key, value) in VALLOX_PROFILE_TO_PRESET_MODE.items()
}
VALLOX_CELL_STATE_TO_STR = {

View file

@ -23,8 +23,8 @@ from .const import (
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
MODE_OFF,
MODE_ON,
PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE,
VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE,
PRESET_MODE_TO_VALLOX_PROFILE,
VALLOX_PROFILE_TO_PRESET_MODE,
)
from .coordinator import ValloxDataUpdateCoordinator
@ -97,7 +97,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
self._client = client
self._attr_unique_id = str(self._device_uuid)
self._attr_preset_modes = list(PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE)
self._attr_preset_modes = list(PRESET_MODE_TO_VALLOX_PROFILE)
@property
def is_on(self) -> bool:
@ -108,7 +108,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
vallox_profile = self.coordinator.data.profile
return VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE.get(vallox_profile)
return VALLOX_PROFILE_TO_PRESET_MODE.get(vallox_profile)
@property
def percentage(self) -> int | None:
@ -204,7 +204,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
return False
try:
profile = PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE[preset_mode]
profile = PRESET_MODE_TO_VALLOX_PROFILE[preset_mode]
await self._client.set_profile(profile)
except ValloxApiException as err:
@ -220,7 +220,7 @@ class ValloxFanEntity(ValloxEntity, FanEntity):
Returns true if speed has been changed, false otherwise.
"""
vallox_profile = (
PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE[preset_mode]
PRESET_MODE_TO_VALLOX_PROFILE[preset_mode]
if preset_mode is not None
else self.coordinator.data.profile
)

View file

@ -45,6 +45,9 @@
},
"set_profile_fan_speed_boost": {
"service": "mdi:speedometer"
},
"set_profile": {
"service": "mdi:fan"
}
}
}

View file

@ -31,7 +31,7 @@ from .const import (
METRIC_KEY_MODE,
MODE_ON,
VALLOX_CELL_STATE_TO_STR,
VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE,
VALLOX_PROFILE_TO_PRESET_MODE,
)
from .coordinator import ValloxDataUpdateCoordinator
@ -78,7 +78,7 @@ class ValloxProfileSensor(ValloxSensorEntity):
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
vallox_profile = self.coordinator.data.profile
return VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE.get(vallox_profile)
return VALLOX_PROFILE_TO_PRESET_MODE.get(vallox_profile)
# There is a quirk with respect to the fan speed reporting. The device keeps on reporting the last

View file

@ -27,3 +27,24 @@ set_profile_fan_speed_boost:
min: 0
max: 100
unit_of_measurement: "%"
set_profile:
fields:
profile:
required: true
selector:
select:
translation_key: "profile"
options:
- "home"
- "away"
- "boost"
- "fireplace"
- "extra"
duration:
required: false
selector:
number:
min: 1
max: 65535
unit_of_measurement: "minutes"

View file

@ -133,6 +133,31 @@
"description": "[%key:component::vallox::services::set_profile_fan_speed_home::fields::fan_speed::description%]"
}
}
},
"set_profile": {
"name": "Activate profile for duration",
"description": "Activate a profile and optionally set duration.",
"fields": {
"profile": {
"name": "Profile",
"description": "Profile to activate"
},
"duration": {
"name": "Duration",
"description": "Activation duration, if omitted device uses stored duration. Duration of 65535 activates profile without timeout. Duration only applies to Boost, Fireplace and Extra profiles."
}
}
}
},
"selector": {
"profile": {
"options": {
"home": "Home",
"away": "Away",
"boost": "Boost",
"fireplace": "Fireplace",
"extra": "Extra"
}
}
}
}

View file

@ -4,7 +4,11 @@ import pytest
from vallox_websocket_api import Profile
from homeassistant.components.vallox import (
ATTR_DURATION,
ATTR_PROFILE,
ATTR_PROFILE_FAN_SPEED,
I18N_KEY_TO_VALLOX_PROFILE,
SERVICE_SET_PROFILE,
SERVICE_SET_PROFILE_FAN_SPEED_AWAY,
SERVICE_SET_PROFILE_FAN_SPEED_BOOST,
SERVICE_SET_PROFILE_FAN_SPEED_HOME,
@ -12,7 +16,7 @@ from homeassistant.components.vallox import (
from homeassistant.components.vallox.const import DOMAIN
from homeassistant.core import HomeAssistant
from .conftest import patch_set_fan_speed
from .conftest import patch_set_fan_speed, patch_set_profile
from tests.common import MockConfigEntry
@ -47,3 +51,45 @@ async def test_create_service(
# Assert
set_fan_speed.assert_called_once_with(profile, 30)
@pytest.mark.parametrize(
("profile", "duration"),
[
("home", None),
("home", 15),
("away", None),
("away", 15),
("boost", None),
("boost", 15),
("fireplace", None),
("fireplace", 15),
("extra", None),
("extra", 15),
],
)
async def test_set_profile_service(
hass: HomeAssistant, mock_entry: MockConfigEntry, profile: str, duration: int | None
) -> None:
"""Test service for setting profile and duration."""
# Act
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
with patch_set_profile() as set_profile:
service_data = {ATTR_PROFILE: profile} | (
{ATTR_DURATION: duration} if duration is not None else {}
)
await hass.services.async_call(
DOMAIN,
SERVICE_SET_PROFILE,
service_data=service_data,
)
await hass.async_block_till_done()
# Assert
set_profile.assert_called_once_with(
I18N_KEY_TO_VALLOX_PROFILE[profile], duration
)