diff --git a/custom_components/wundasmart/climate.py b/custom_components/wundasmart/climate.py index e308073..42dee57 100644 --- a/custom_components/wundasmart/climate.py +++ b/custom_components/wundasmart/climate.py @@ -10,6 +10,8 @@ ClimateEntityFeature, HVACAction, HVACMode, + PRESET_ECO, + PRESET_COMFORT ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -37,6 +39,20 @@ HVACMode.HEAT, ] +PRESET_REDUCED = "reduced" + +SUPPORTED_PRESET_MODES = [ + PRESET_REDUCED, + PRESET_ECO, + PRESET_COMFORT +] + +PRESET_MODE_STATE_KEYS = { + PRESET_REDUCED: "t_lo", + PRESET_ECO: "t_norm", + PRESET_COMFORT: "t_hi" +} + PARALLEL_UPDATES = 1 @@ -71,6 +87,8 @@ class Device(CoordinatorEntity[WundasmartDataUpdateCoordinator], ClimateEntity): _attr_hvac_modes = SUPPORTED_HVAC_MODES _attr_temperature_unit = TEMP_CELSIUS + _attr_preset_modes = SUPPORTED_PRESET_MODES + _attr_translation_key = DOMAIN def __init__( self, @@ -93,11 +111,14 @@ def __init__( self._attr_unique_id = device["id"] self._attr_type = device["device_type"] self._attr_device_info = coordinator.device_info - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) self._attr_current_temperature = None self._attr_target_temperature = None self._attr_current_humidity = None self._attr_hvac_mode = HVACMode.AUTO + self._attr_preset_mode = None # Update with initial state self.__update_state() @@ -166,6 +187,26 @@ def __set_target_temperature(self): except (ValueError, TypeError): _LOGGER.warning(f"Unexpected set temp value '{state['temp']}' for {self._attr_name}") + def __set_preset_mode(self): + state = self.__state + try: + set_temp = float(state.get("temp", 0.0)) + except (ValueError, TypeError): + _LOGGER.warning(f"Unexpected set temp value '{state['temp']}' for {self._attr_name}") + return + + for preset_mode, state_key in PRESET_MODE_STATE_KEYS.items(): + if state.get(state_key) is not None: + try: + t_preset = float(self.__state[state_key]) + if t_preset == set_temp: + self._attr_preset_mode = preset_mode + break + except (ValueError, TypeError): + _LOGGER.warning(f"Unexpected {state_key} value '{state[state_key]}' for {self._attr_name}") + else: + self._attr_preset_mode = None + def __set_hvac_state(self): """Set the hvac action and hvac mode from the coordinator data.""" state = self.__state @@ -195,6 +236,7 @@ def __update_state(self): self.__set_current_temperature() self.__set_current_humidity() self.__set_target_temperature() + self.__set_preset_mode() self.__set_hvac_state() @callback @@ -254,3 +296,27 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode): # Fetch the updated state await self.coordinator.async_request_refresh() + + async def async_set_preset_mode(self, preset_mode) -> None: + state_key = PRESET_MODE_STATE_KEYS.get(preset_mode) + if state_key is None: + raise NotImplementedError(f"Unsupported Preset mode {preset_mode}") + + t_preset = float(self.__state[state_key]) + + await send_command( + self._session, + self._wunda_ip, + self._wunda_user, + self._wunda_pass, + params={ + "cmd": 1, + "roomid": self._wunda_id, + "temp": t_preset, + "locktt": 0, + "time": 0, + }, + ) + + # Fetch the updated state + await self.coordinator.async_request_refresh() diff --git a/custom_components/wundasmart/sensor.py b/custom_components/wundasmart/sensor.py index 89da27a..f022e06 100644 --- a/custom_components/wundasmart/sensor.py +++ b/custom_components/wundasmart/sensor.py @@ -178,6 +178,8 @@ async def async_setup_entry( class Sensor(CoordinatorEntity[WundasmartDataUpdateCoordinator], SensorEntity): """Sensor entity for WundaSmart sensor values.""" + _attr_translation_key = DOMAIN + def __init__( self, wunda_id: str, diff --git a/custom_components/wundasmart/strings.json b/custom_components/wundasmart/strings.json index 353cce9..481f596 100644 --- a/custom_components/wundasmart/strings.json +++ b/custom_components/wundasmart/strings.json @@ -19,5 +19,18 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "climate": { + "wundasmart": { + "state_attributes": { + "preset_mode": { + "state": { + "reduced": "Reduced" + } + } + } + } + } } } diff --git a/custom_components/wundasmart/translations/en.json b/custom_components/wundasmart/translations/en.json index 5d210af..1856a5e 100644 --- a/custom_components/wundasmart/translations/en.json +++ b/custom_components/wundasmart/translations/en.json @@ -19,5 +19,18 @@ "title": "Wundasmart configuration" } } + }, + "entity": { + "climate": { + "wundasmart": { + "state_attributes": { + "preset_mode": { + "state": { + "reduced": "Reduced" + } + } + } + } + } } } diff --git a/custom_components/wundasmart/water_heater.py b/custom_components/wundasmart/water_heater.py index a895733..7f124ee 100644 --- a/custom_components/wundasmart/water_heater.py +++ b/custom_components/wundasmart/water_heater.py @@ -75,10 +75,9 @@ class Device(CoordinatorEntity[WundasmartDataUpdateCoordinator], WaterHeaterEnti OPERATION_BOOST_ON, OPERATION_BOOST_OFF ] - _attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE - _attr_temperature_unit = TEMP_CELSIUS + _attr_translation_key = DOMAIN def __init__( self, diff --git a/tests/fixtures/test_set_presets.json b/tests/fixtures/test_set_presets.json new file mode 100644 index 0000000..be12241 --- /dev/null +++ b/tests/fixtures/test_set_presets.json @@ -0,0 +1,95 @@ +{ + "state": true, + "devices": { + "0": { + "device_type": "wunda", + "state": { + "hw_mode": "2", + "hw_mode_state": "1", + "hw_boost_state": "0", + "hw_sp": "45", + "hw_hist": "5", + "hw_ext_temp": "0" + }, + "device_name": "Smart%20HubSwitch", + "id": "wunda.0" + }, + "1": { + "device_type": "SENSOR", + "id": "SENSOR.1", + "state": { + "s": "1", + "t": "BT6", + "v": "1.8", + "temp": "17.8", + "rh": "66.57", + "temp_ext": "", + "ext": "0", + "bat": "100", + "sig": "88", + "alarm": "0" + } + }, + "31": { + "device_type": "TRV", + "id": "TRV.31", + "state": { + "s": "1", + "t": "TH3K", + "v": "1.2", + "vtemp": "15.20", + "bat": "100", + "sig": "88", + "room_id": "0", + "vpos_min": "5", + "vpos_range": "40", + "downforce": "0", + "alarm": "0", + "trv_range": "716" + } + }, + "121": { + "device_type": "ROOM", + "state": { + "t_lo": "14.00", + "t_norm": "19.00", + "t_hi": "21.00", + "heat": "4", + "temp_pre": "0", + "prg": "1", + "lock": "0", + "temp": "21.00", + "ntemp": "-1.00", + "ntime": "0", + "es_avgtime": "0", + "zone": "1", + "relays": "0", + "hb0": "0", + "hb1": "0", + "enable": "9", + "tmax": "27", + "tmaxh": "5", + "settime": "7200", + "pic": "3", + "loc_in": "1", + "loc_out": "3", + "alarm": "0", + "tbl": "555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555" + }, + "name": "Test%20Room", + "id": "ROOM.121", + "sensor_state": { + "s": "1", + "t": "BT6", + "v": "1.8", + "temp": "17.8", + "rh": "66.57", + "temp_ext": "", + "ext": "0", + "bat": "100", + "sig": "88", + "alarm": "0" + } + } + } +} \ No newline at end of file diff --git a/tests/test_climate.py b/tests/test_climate.py index 36e4827..cd494f1 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -117,3 +117,59 @@ async def test_hvac_mode_when_manually_turned_off(hass: HomeAssistant, config): assert state assert state.state == HVACAction.OFF assert state.attributes["hvac_action"] == HVACAction.OFF + + +async def test_set_presets(hass: HomeAssistant, config): + entry = MockConfigEntry(domain=DOMAIN, data=config) + entry.add_to_hass(hass) + + data = deserialize_get_devices_fixture(load_fixture("test_set_presets.json")) + with patch("custom_components.wundasmart.get_devices", return_value=data), \ + patch("custom_components.wundasmart.climate.send_command", return_value=None) as mock: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("climate.test_room") + + assert state + assert state.attributes["temperature"] == 21.0 + assert state.attributes["preset_mode"] == "comfort" + + # set the preset 'reduced' + await hass.services.async_call("climate", "set_preset_mode", { + "entity_id": "climate.test_room", + "preset_mode": "reduced" + }) + await hass.async_block_till_done() + + # Check send_command was called correctly + assert mock.call_count == 1 + assert mock.call_args.kwargs["params"] + assert mock.call_args.kwargs["params"]["roomid"] == 121 + assert mock.call_args.kwargs["params"]["temp"] == 14.0 + + # set the preset 'eco' + await hass.services.async_call("climate", "set_preset_mode", { + "entity_id": "climate.test_room", + "preset_mode": "eco" + }) + await hass.async_block_till_done() + + # Check send_command was called correctly + assert mock.call_count == 2 + assert mock.call_args.kwargs["params"] + assert mock.call_args.kwargs["params"]["roomid"] == 121 + assert mock.call_args.kwargs["params"]["temp"] == 19.0 + + # set the preset 'comfort' + await hass.services.async_call("climate", "set_preset_mode", { + "entity_id": "climate.test_room", + "preset_mode": "comfort" + }) + await hass.async_block_till_done() + + # Check send_command was called correctly + assert mock.call_count == 3 + assert mock.call_args.kwargs["params"] + assert mock.call_args.kwargs["params"]["roomid"] == 121 + assert mock.call_args.kwargs["params"]["temp"] == 21.0