mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 04:05:20 +01:00
Update Saunum integration to platinum quality (#160824)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -455,6 +455,7 @@ homeassistant.components.russound_rio.*
|
|||||||
homeassistant.components.ruuvi_gateway.*
|
homeassistant.components.ruuvi_gateway.*
|
||||||
homeassistant.components.ruuvitag_ble.*
|
homeassistant.components.ruuvitag_ble.*
|
||||||
homeassistant.components.samsungtv.*
|
homeassistant.components.samsungtv.*
|
||||||
|
homeassistant.components.saunum.*
|
||||||
homeassistant.components.scene.*
|
homeassistant.components.scene.*
|
||||||
homeassistant.components.schedule.*
|
homeassistant.components.schedule.*
|
||||||
homeassistant.components.schlage.*
|
homeassistant.components.schlage.*
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from homeassistant.components.climate import (
|
|||||||
HVACAction,
|
HVACAction,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
@@ -57,6 +57,8 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
|
|||||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
|
||||||
)
|
)
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_precision = PRECISION_WHOLE
|
||||||
|
_attr_target_temperature_step = 1.0
|
||||||
_attr_min_temp = MIN_TEMPERATURE
|
_attr_min_temp = MIN_TEMPERATURE
|
||||||
_attr_max_temp = MAX_TEMPERATURE
|
_attr_max_temp = MAX_TEMPERATURE
|
||||||
_attr_fan_modes = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
_attr_fan_modes = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
|
||||||
@@ -143,10 +145,18 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
|
|||||||
"""Set new fan mode."""
|
"""Set new fan mode."""
|
||||||
if not self.coordinator.data.session_active:
|
if not self.coordinator.data.session_active:
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
"Cannot change fan mode when sauna session is not active",
|
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="session_not_active",
|
translation_key="session_not_active",
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.coordinator.client.async_set_fan_speed(FAN_MODE_TO_SPEED[fan_mode])
|
try:
|
||||||
|
await self.coordinator.client.async_set_fan_speed(
|
||||||
|
FAN_MODE_TO_SPEED[fan_mode]
|
||||||
|
)
|
||||||
|
except SaunumException as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="set_fan_mode_failed",
|
||||||
|
) from err
|
||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from pysaunum import SaunumException
|
from pysaunum import SaunumException
|
||||||
|
|
||||||
@@ -15,6 +15,9 @@ from . import LeilSaunaConfigEntry
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import LeilSaunaEntity
|
from .entity import LeilSaunaEntity
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .coordinator import LeilSaunaCoordinator
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ class LeilSaunaLight(LeilSaunaEntity, LightEntity):
|
|||||||
_attr_color_mode = ColorMode.ONOFF
|
_attr_color_mode = ColorMode.ONOFF
|
||||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||||
|
|
||||||
def __init__(self, coordinator) -> None:
|
def __init__(self, coordinator: LeilSaunaCoordinator) -> None:
|
||||||
"""Initialize the light entity."""
|
"""Initialize the light entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
# Override unique_id to differentiate from climate entity
|
# Override unique_id to differentiate from climate entity
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pysaunum"],
|
"loggers": ["pysaunum"],
|
||||||
"quality_scale": "gold",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pysaunum==0.2.0"]
|
"requirements": ["pysaunum==0.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,11 +133,7 @@ class LeilSaunaNumber(LeilSaunaEntity, NumberEntity):
|
|||||||
except SaunumException as err:
|
except SaunumException as err:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="set_value_failed",
|
translation_key=f"set_{self.entity_description.key}_failed",
|
||||||
translation_placeholders={
|
|
||||||
"entity": self.entity_description.key,
|
|
||||||
"value": str(value),
|
|
||||||
},
|
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|||||||
@@ -77,4 +77,4 @@ rules:
|
|||||||
inject-websession:
|
inject-websession:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: Integration uses Modbus TCP protocol and does not make HTTP requests.
|
comment: Integration uses Modbus TCP protocol and does not make HTTP requests.
|
||||||
strict-typing: todo
|
strict-typing: done
|
||||||
|
|||||||
@@ -89,6 +89,12 @@
|
|||||||
"session_not_active": {
|
"session_not_active": {
|
||||||
"message": "Cannot change fan mode when sauna session is not active"
|
"message": "Cannot change fan mode when sauna session is not active"
|
||||||
},
|
},
|
||||||
|
"set_fan_duration_failed": {
|
||||||
|
"message": "Failed to set fan duration"
|
||||||
|
},
|
||||||
|
"set_fan_mode_failed": {
|
||||||
|
"message": "Failed to set fan mode"
|
||||||
|
},
|
||||||
"set_hvac_mode_failed": {
|
"set_hvac_mode_failed": {
|
||||||
"message": "Failed to set HVAC mode to {hvac_mode}"
|
"message": "Failed to set HVAC mode to {hvac_mode}"
|
||||||
},
|
},
|
||||||
@@ -98,11 +104,11 @@
|
|||||||
"set_light_on_failed": {
|
"set_light_on_failed": {
|
||||||
"message": "Failed to turn on light"
|
"message": "Failed to turn on light"
|
||||||
},
|
},
|
||||||
|
"set_sauna_duration_failed": {
|
||||||
|
"message": "Failed to set sauna duration"
|
||||||
|
},
|
||||||
"set_temperature_failed": {
|
"set_temperature_failed": {
|
||||||
"message": "Failed to set temperature to {temperature}"
|
"message": "Failed to set temperature to {temperature}"
|
||||||
},
|
|
||||||
"set_value_failed": {
|
|
||||||
"message": "Failed to set {entity} to {value}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
mypy.ini
generated
10
mypy.ini
generated
@@ -4306,6 +4306,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.saunum.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.scene.*]
|
[mypy-homeassistant.components.scene.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
]),
|
]),
|
||||||
'max_temp': 100,
|
'max_temp': 100,
|
||||||
'min_temp': 40,
|
'min_temp': 40,
|
||||||
|
'target_temp_step': 1.0,
|
||||||
}),
|
}),
|
||||||
'config_entry_id': <ANY>,
|
'config_entry_id': <ANY>,
|
||||||
'config_subentry_id': <ANY>,
|
'config_subentry_id': <ANY>,
|
||||||
@@ -51,7 +52,7 @@
|
|||||||
# name: test_entities[climate.saunum_leil-state]
|
# name: test_entities[climate.saunum_leil-state]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
'current_temperature': 75.0,
|
'current_temperature': 75,
|
||||||
'fan_mode': 'medium',
|
'fan_mode': 'medium',
|
||||||
'fan_modes': list([
|
'fan_modes': list([
|
||||||
'off',
|
'off',
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
'max_temp': 100,
|
'max_temp': 100,
|
||||||
'min_temp': 40,
|
'min_temp': 40,
|
||||||
'supported_features': <ClimateEntityFeature: 9>,
|
'supported_features': <ClimateEntityFeature: 9>,
|
||||||
|
'target_temp_step': 1.0,
|
||||||
'temperature': 80,
|
'temperature': 80,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
|
|||||||
@@ -378,3 +378,33 @@ async def test_fan_mode_session_not_active_error(
|
|||||||
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW},
|
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_integration")
|
||||||
|
async def test_fan_mode_error_handling(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_saunum_client,
|
||||||
|
) -> None:
|
||||||
|
"""Test error handling when setting fan mode fails."""
|
||||||
|
entity_id = "climate.saunum_leil"
|
||||||
|
|
||||||
|
# Ensure session is active
|
||||||
|
mock_saunum_client.async_get_data.return_value.session_active = True
|
||||||
|
|
||||||
|
# Make the client method raise an exception
|
||||||
|
mock_saunum_client.async_set_fan_speed.side_effect = SaunumException(
|
||||||
|
"Communication error"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to call the service and expect HomeAssistantError
|
||||||
|
with pytest.raises(HomeAssistantError) as exc_info:
|
||||||
|
await hass.services.async_call(
|
||||||
|
CLIMATE_DOMAIN,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the exception has the correct translation key
|
||||||
|
assert exc_info.value.translation_key == "set_fan_mode_failed"
|
||||||
|
assert exc_info.value.translation_domain == "saunum"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ async def test_sensor_not_created_when_value_is_none(
|
|||||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("sensor.saunum_leil_temperature") is None
|
assert hass.states.get("sensor.saunum_leil_current_temperature") is None
|
||||||
assert hass.states.get("sensor.saunum_leil_heater_elements_active") is None
|
assert hass.states.get("sensor.saunum_leil_heater_elements_active") is None
|
||||||
assert hass.states.get("sensor.saunum_leil_on_time") is None
|
assert hass.states.get("sensor.saunum_leil_on_time") is None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user