Add Celsius temperature support to Hot Spring (#183068)

Co-authored-by: Moustachauve <2206577+Moustachauve@users.noreply.github.com>
This commit is contained in:
Christophe Gagnier
2026-09-25 06:22:47 +01:00
committed by GitHub
co-authored by Moustachauve
parent 159f83af3d
commit 2f3fb26a07
6 changed files with 154 additions and 11 deletions
+49 -5
View File
@@ -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)
+15 -2
View File
@@ -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:
@@ -60,3 +60,22 @@
'state': '40.0',
})
# ---
# name: test_set_target_temperature_celsius
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'ConnectedSpa_DDEEFF Target temperature',
<NumberEntityCapabilityAttribute.MAX: 'max'>: 40.0,
<NumberEntityCapabilityAttribute.MIN: 'min'>: 26.0,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.AUTO: 'auto'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 0.5,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.connectedspa_ddeeff_target_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '38.5',
})
# ---
@@ -370,3 +370,19 @@
'state': '1.0.0',
})
# ---
# name: test_temperature_sensor_celsius
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'ConnectedSpa_DDEEFF Current temperature',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.connectedspa_ddeeff_current_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '38.5',
})
# ---
+39 -4
View File
@@ -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"
+16
View File
@@ -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