diff --git a/homeassistant/components/hotspring/number.py b/homeassistant/components/hotspring/number.py index a97056fe27b3..9625414e9831 100644 --- a/homeassistant/components/hotspring/number.py +++ b/homeassistant/components/hotspring/number.py @@ -2,6 +2,8 @@ from typing import override +from hotspring import TemperatureUnit + from homeassistant.components.number import ( NumberDeviceClass, NumberEntity, @@ -17,14 +19,18 @@ from .helpers import hotspring_exception_handler PARALLEL_UPDATES = 1 +MIN_TEMP_FAHRENHEIT = 80.0 +MAX_TEMP_FAHRENHEIT = 104.0 +STEP_FAHRENHEIT = 1.0 + +MIN_TEMP_CELSIUS = 26.0 +MAX_TEMP_CELSIUS = 40.0 +STEP_CELSIUS = 0.5 + TARGET_TEMPERATURE_DESCRIPTION = NumberEntityDescription( key="target_temperature", translation_key="target_temperature", device_class=NumberDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, - native_min_value=80.0, - native_max_value=104.0, - native_step=1.0, ) @@ -53,6 +59,43 @@ class HotSpringNumberEntity(HotSpringEntity, NumberEntity): super().__init__(coordinator, description.key) self.entity_description = description + @property + def _is_celsius(self) -> bool: + """Return True if the spa is configured in Celsius.""" + return self.coordinator.data.heater.temperature_unit is TemperatureUnit.CELSIUS + + @property + @override + def native_unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + if self._is_celsius: + return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT + + @property + @override + def native_min_value(self) -> float: + """Return the minimum value.""" + if self._is_celsius: + return MIN_TEMP_CELSIUS + return MIN_TEMP_FAHRENHEIT + + @property + @override + def native_max_value(self) -> float: + """Return the maximum value.""" + if self._is_celsius: + return MAX_TEMP_CELSIUS + return MAX_TEMP_FAHRENHEIT + + @property + @override + def native_step(self) -> float: + """Return the step value.""" + if self._is_celsius: + return STEP_CELSIUS + return STEP_FAHRENHEIT + @property @override def native_value(self) -> float | None: @@ -63,5 +106,6 @@ class HotSpringNumberEntity(HotSpringEntity, NumberEntity): @override async def async_set_native_value(self, value: float) -> None: """Set the target temperature.""" - await self.coordinator.hotspring.set_temperature(round(value)) + target = round(value / self.native_step) * self.native_step + await self.coordinator.hotspring.set_temperature(target) self.coordinator.async_set_updated_data(self.coordinator.data) diff --git a/homeassistant/components/hotspring/sensor.py b/homeassistant/components/hotspring/sensor.py index d8e7eb2f5e42..4d5aaa8b95d7 100644 --- a/homeassistant/components/hotspring/sensor.py +++ b/homeassistant/components/hotspring/sensor.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import override -from hotspring import Spa +from hotspring import Spa, TemperatureUnit from homeassistant.components.sensor import ( SensorDeviceClass, @@ -29,6 +29,7 @@ class HotSpringSensorEntityDescription(SensorEntityDescription): exists_fn: Callable[[Spa], bool] = lambda _: True value_fn: Callable[[Spa], StateType] + unit_fn: Callable[[Spa], str | None] | None = None SENSORS: tuple[HotSpringSensorEntityDescription, ...] = ( @@ -37,8 +38,12 @@ SENSORS: tuple[HotSpringSensorEntityDescription, ...] = ( translation_key="current_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, value_fn=lambda spa: spa.heater.current_temperature, + unit_fn=lambda spa: ( + UnitOfTemperature.CELSIUS + if spa.heater.temperature_unit is TemperatureUnit.CELSIUS + else UnitOfTemperature.FAHRENHEIT + ), ), HotSpringSensorEntityDescription( key="water_care_120_day_timer", @@ -118,6 +123,14 @@ class HotSpringSensorEntity(HotSpringEntity, SensorEntity): super().__init__(coordinator, description.key) self.entity_description = description + @property + @override + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + if self.entity_description.unit_fn is not None: + return self.entity_description.unit_fn(self.coordinator.data) + return super().native_unit_of_measurement + @property @override def native_value(self) -> StateType: diff --git a/tests/components/hotspring/snapshots/test_number.ambr b/tests/components/hotspring/snapshots/test_number.ambr index 770495a2b5f5..8caa3c01dbc7 100644 --- a/tests/components/hotspring/snapshots/test_number.ambr +++ b/tests/components/hotspring/snapshots/test_number.ambr @@ -60,3 +60,22 @@ 'state': '40.0', }) # --- +# name: test_set_target_temperature_celsius + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'ConnectedSpa_DDEEFF Target temperature', + : 40.0, + : 26.0, + : , + : 0.5, + : , + }), + 'context': , + 'entity_id': 'number.connectedspa_ddeeff_target_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.5', + }) +# --- diff --git a/tests/components/hotspring/snapshots/test_sensor.ambr b/tests/components/hotspring/snapshots/test_sensor.ambr index 15e8bcbbc99a..2829a39afaea 100644 --- a/tests/components/hotspring/snapshots/test_sensor.ambr +++ b/tests/components/hotspring/snapshots/test_sensor.ambr @@ -370,3 +370,19 @@ 'state': '1.0.0', }) # --- +# name: test_temperature_sensor_celsius + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'ConnectedSpa_DDEEFF Current temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.connectedspa_ddeeff_current_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.5', + }) +# --- diff --git a/tests/components/hotspring/test_number.py b/tests/components/hotspring/test_number.py index c1fe201bafd9..617331a20c7a 100644 --- a/tests/components/hotspring/test_number.py +++ b/tests/components/hotspring/test_number.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from hotspring import HotSpringConnectionError, HotSpringError, Spa +from hotspring import HotSpringConnectionError, HotSpringError, Spa, TemperatureUnit import pytest from syrupy.assertion import SnapshotAssertion @@ -45,8 +45,8 @@ async def test_set_target_temperature( assert (state := hass.states.get(ENTITY_ID)) assert state.state == "40.0" - def set_temp_mock(value: int) -> None: - device_fixture.heater.set_temperature = float(value) + def set_temp_mock(value: float) -> None: + device_fixture.heater.set_temperature = value mock_hotspring.set_temperature.side_effect = set_temp_mock @@ -60,7 +60,7 @@ async def test_set_target_temperature( blocking=True, ) - mock_hotspring.set_temperature.assert_called_once_with(100) + mock_hotspring.set_temperature.assert_called_once_with(100.0) mock_hotspring.update.assert_called_once() assert (state := hass.states.get(ENTITY_ID)) assert state.state == "37.8" @@ -96,3 +96,38 @@ async def test_set_target_temperature_error( }, blocking=True, ) + + +async def test_set_target_temperature_celsius( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hotspring: MagicMock, + device_fixture: Spa, + snapshot: SnapshotAssertion, +) -> None: + """Test setting target temperature when the spa is configured in Celsius.""" + device_fixture.heater.temperature_unit = TemperatureUnit.CELSIUS + device_fixture.heater.set_temperature = 38.5 + + def set_temp_mock(value: float) -> None: + device_fixture.heater.set_temperature = value + + mock_hotspring.set_temperature.side_effect = set_temp_mock + + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + + assert hass.states.get(ENTITY_ID) == snapshot + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 37.5, + }, + blocking=True, + ) + + mock_hotspring.set_temperature.assert_called_once_with(37.5) + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == "37.5" diff --git a/tests/components/hotspring/test_sensor.py b/tests/components/hotspring/test_sensor.py index 970913b19a8a..14ebcbed3278 100644 --- a/tests/components/hotspring/test_sensor.py +++ b/tests/components/hotspring/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the Hot Spring sensor platform.""" +from hotspring import Spa, TemperatureUnit import pytest from syrupy.assertion import SnapshotAssertion @@ -22,3 +23,18 @@ async def test_sensors( """Test the sensor platform state.""" await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_hotspring") +async def test_temperature_sensor_celsius( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_fixture: Spa, + snapshot: SnapshotAssertion, +) -> None: + """Test the temperature sensor when the spa is configured in Celsius.""" + device_fixture.heater.temperature_unit = TemperatureUnit.CELSIUS + device_fixture.heater.current_temperature = 38.5 + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + assert hass.states.get("sensor.connectedspa_ddeeff_current_temperature") == snapshot