diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 9c3c5c111f5..208e61550e6 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -7,12 +7,15 @@ from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.components.climate import ( + ATTR_FAN_MODE, + ATTR_SWING_MODE, ClimateEntity, ClimateEntityFeature, HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_MODE, ATTR_STATE, ATTR_TEMPERATURE, PRECISION_TENTHS, @@ -34,12 +37,16 @@ SERVICE_ENABLE_TIMER = "enable_timer" ATTR_MINUTES = "minutes" SERVICE_ENABLE_PURE_BOOST = "enable_pure_boost" SERVICE_DISABLE_PURE_BOOST = "disable_pure_boost" +SERVICE_FULL_STATE = "full_state" ATTR_AC_INTEGRATION = "ac_integration" ATTR_GEO_INTEGRATION = "geo_integration" ATTR_INDOOR_INTEGRATION = "indoor_integration" ATTR_OUTDOOR_INTEGRATION = "outdoor_integration" ATTR_SENSITIVITY = "sensitivity" +ATTR_TARGET_TEMPERATURE = "target_temperature" +ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode" +ATTR_LIGHT = "light" BOOST_INCLUSIVE = "boost_inclusive" PARALLEL_UPDATES = 0 @@ -118,6 +125,20 @@ async def async_setup_entry( }, "async_enable_pure_boost", ) + platform.async_register_entity_service( + SERVICE_FULL_STATE, + { + vol.Required(ATTR_MODE): vol.In( + ["cool", "heat", "fan", "auto", "dry", "off"] + ), + vol.Optional(ATTR_TARGET_TEMPERATURE): int, + vol.Optional(ATTR_FAN_MODE): str, + vol.Optional(ATTR_SWING_MODE): str, + vol.Optional(ATTR_HORIZONTAL_SWING_MODE): str, + vol.Optional(ATTR_LIGHT): vol.In(["on", "off"]), + }, + "async_full_ac_state", + ) class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @@ -335,6 +356,37 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): assumed_state=True, ) + async def async_full_ac_state( + self, + mode: str, + target_temperature: int | None = None, + fan_mode: str | None = None, + swing_mode: str | None = None, + horizontal_swing_mode: str | None = None, + light: str | None = None, + ) -> None: + """Set full AC state.""" + new_ac_state = self.device_data.ac_states + new_ac_state.pop("timestamp") + new_ac_state["on"] = False + if mode != "off": + new_ac_state["on"] = True + new_ac_state["mode"] = mode + if target_temperature: + new_ac_state["targetTemperature"] = target_temperature + if fan_mode: + new_ac_state["fanLevel"] = fan_mode + if swing_mode: + new_ac_state["swing"] = swing_mode + if horizontal_swing_mode: + new_ac_state["horizontalSwing"] = horizontal_swing_mode + if light: + new_ac_state["light"] = light + + await self.api_call_custom_service_full_ac_state( + key="hvac_mode", value=mode, data=new_ac_state + ) + async def async_enable_timer(self, minutes: int) -> None: """Enable the timer.""" new_state = bool(self.device_data.ac_states["on"] is False) @@ -419,3 +471,14 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): result = {} result = await self._client.async_set_pureboost(self._device_id, data) return bool(result.get("status") == "success") + + @async_handle_api_call + async def api_call_custom_service_full_ac_state( + self, + key: str, + value: Any, + data: dict, + ) -> bool: + """Make service call to api.""" + result = await self._client.async_set_ac_states(self._device_id, data) + return bool(result.get("result", {}).get("status") == "Success") diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index 9ce13b70eaa..5ff78b2c34e 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -80,3 +80,69 @@ enable_pure_boost: options: - "Normal" - "Sensitive" +full_state: + name: Set full state + description: Set full state for Sensibo device + target: + entity: + integration: sensibo + domain: climate + fields: + mode: + name: HVAC mode + description: HVAC mode to set + required: true + example: "heat" + selector: + select: + options: + - "cool" + - "heat" + - "fan" + - "auto" + - "dry" + - "off" + target_temperature: + name: Target Temperature + description: Optionally set target temperature + required: false + example: 23 + selector: + number: + min: 0 + step: 1 + mode: box + fan_mode: + name: Fan mode + description: Optionally set fan mode + required: false + example: "low" + selector: + text: + type: text + swing_mode: + name: swing mode + description: Optionally set swing mode + required: false + example: "fixedBottom" + selector: + text: + type: text + horizontal_swing_mode: + name: Horizontal swing mode + description: Optionally set horizontal swing mode + required: false + example: "fixedLeft" + selector: + text: + type: text + light: + name: Light + description: Set light on or off + required: false + example: "on" + selector: + select: + options: + - "on" + - "off" diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index f9c3a7cb301..555fc7f2ea5 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -23,19 +23,24 @@ from homeassistant.components.climate import ( from homeassistant.components.sensibo.climate import ( ATTR_AC_INTEGRATION, ATTR_GEO_INTEGRATION, + ATTR_HORIZONTAL_SWING_MODE, ATTR_INDOOR_INTEGRATION, + ATTR_LIGHT, ATTR_MINUTES, ATTR_OUTDOOR_INTEGRATION, ATTR_SENSITIVITY, + ATTR_TARGET_TEMPERATURE, SERVICE_ASSUME_STATE, SERVICE_ENABLE_PURE_BOOST, SERVICE_ENABLE_TIMER, + SERVICE_FULL_STATE, _find_valid_target_temp, ) from homeassistant.components.sensibo.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_STATE, ATTR_TEMPERATURE, SERVICE_TURN_OFF, @@ -916,3 +921,91 @@ async def test_climate_pure_boost( assert state2.state == "on" assert state3.state == "on" assert state4.state == "s" + + +async def test_climate_full_ac_state( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo climate Full AC state service.""" + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state_climate = hass.states.get("climate.hallway") + assert state_climate.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", + ): + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_FULL_STATE, + { + ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_TARGET_TEMPERATURE: 22, + }, + blocking=True, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_FULL_STATE, + { + ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_MODE: "cool", + ATTR_TARGET_TEMPERATURE: 22, + ATTR_FAN_MODE: "high", + ATTR_SWING_MODE: "stopped", + ATTR_HORIZONTAL_SWING_MODE: "stopped", + ATTR_LIGHT: "on", + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "cool") + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "target_temp", 22) + monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_mode", "high") + monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_mode", "stopped") + monkeypatch.setattr( + get_data.parsed["ABC999111"], "horizontal_swing_mode", "stopped" + ) + monkeypatch.setattr(get_data.parsed["ABC999111"], "light_mode", "on") + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state = hass.states.get("climate.hallway") + + assert state.state == "cool" + assert state.attributes["temperature"] == 22