Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3680e39

Browse files
Add climate platform to eheimdigital (home-assistant#135878)
1 parent 661bacd commit 3680e39

File tree

7 files changed

+483
-5
lines changed

7 files changed

+483
-5
lines changed

‎homeassistant/components/eheimdigital/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .const import DOMAIN
1111
from .coordinator import EheimDigitalUpdateCoordinator
1212

13-
PLATFORMS = [Platform.LIGHT]
13+
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT]
1414

1515
type EheimDigitalConfigEntry = ConfigEntry[EheimDigitalUpdateCoordinator]
1616

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""EHEIM Digital climate."""
2+
3+
from typing import Any
4+
5+
from eheimdigital.heater import EheimDigitalHeater
6+
from eheimdigital.types import EheimDigitalClientError, HeaterMode, HeaterUnit
7+
8+
from homeassistant.components.climate import (
9+
PRESET_NONE,
10+
ClimateEntity,
11+
ClimateEntityFeature,
12+
HVACAction,
13+
HVACMode,
14+
)
15+
from homeassistant.const import (
16+
ATTR_TEMPERATURE,
17+
PRECISION_HALVES,
18+
PRECISION_TENTHS,
19+
UnitOfTemperature,
20+
)
21+
from homeassistant.core import HomeAssistant
22+
from homeassistant.exceptions import HomeAssistantError
23+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
24+
25+
from . import EheimDigitalConfigEntry
26+
from .const import HEATER_BIO_MODE, HEATER_PRESET_TO_HEATER_MODE, HEATER_SMART_MODE
27+
from .coordinator import EheimDigitalUpdateCoordinator
28+
from .entity import EheimDigitalEntity
29+
30+
# Coordinator is used to centralize the data updates
31+
PARALLEL_UPDATES = 0
32+
33+
34+
async def async_setup_entry(
35+
hass: HomeAssistant,
36+
entry: EheimDigitalConfigEntry,
37+
async_add_entities: AddEntitiesCallback,
38+
) -> None:
39+
"""Set up the callbacks for the coordinator so climate entities can be added as devices are found."""
40+
coordinator = entry.runtime_data
41+
42+
async def async_setup_device_entities(device_address: str) -> None:
43+
"""Set up the light entities for a device."""
44+
device = coordinator.hub.devices[device_address]
45+
46+
if isinstance(device, EheimDigitalHeater):
47+
async_add_entities([EheimDigitalHeaterClimate(coordinator, device)])
48+
49+
coordinator.add_platform_callback(async_setup_device_entities)
50+
51+
for device_address in entry.runtime_data.hub.devices:
52+
await async_setup_device_entities(device_address)
53+
54+
55+
class EheimDigitalHeaterClimate(EheimDigitalEntity[EheimDigitalHeater], ClimateEntity):
56+
"""Represent an EHEIM Digital heater."""
57+
58+
_attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO]
59+
_attr_hvac_mode = HVACMode.OFF
60+
_attr_precision = PRECISION_TENTHS
61+
_attr_supported_features = (
62+
ClimateEntityFeature.TARGET_TEMPERATURE
63+
| ClimateEntityFeature.TURN_ON
64+
| ClimateEntityFeature.TURN_OFF
65+
| ClimateEntityFeature.PRESET_MODE
66+
)
67+
_attr_target_temperature_step = PRECISION_HALVES
68+
_attr_preset_modes = [PRESET_NONE, HEATER_BIO_MODE, HEATER_SMART_MODE]
69+
_attr_temperature_unit = UnitOfTemperature.CELSIUS
70+
_attr_preset_mode = PRESET_NONE
71+
_attr_translation_key = "heater"
72+
73+
def __init__(
74+
self, coordinator: EheimDigitalUpdateCoordinator, device: EheimDigitalHeater
75+
) -> None:
76+
"""Initialize an EHEIM Digital thermocontrol climate entity."""
77+
super().__init__(coordinator, device)
78+
self._attr_unique_id = self._device_address
79+
self._async_update_attrs()
80+
81+
async def async_set_preset_mode(self, preset_mode: str) -> None:
82+
"""Set the preset mode."""
83+
try:
84+
if preset_mode in HEATER_PRESET_TO_HEATER_MODE:
85+
await self._device.set_operation_mode(
86+
HEATER_PRESET_TO_HEATER_MODE[preset_mode]
87+
)
88+
except EheimDigitalClientError as err:
89+
raise HomeAssistantError from err
90+
91+
async def async_set_temperature(self, **kwargs: Any) -> None:
92+
"""Set a new temperature."""
93+
try:
94+
if ATTR_TEMPERATURE in kwargs:
95+
await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE])
96+
except EheimDigitalClientError as err:
97+
raise HomeAssistantError from err
98+
99+
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
100+
"""Set the heating mode."""
101+
try:
102+
match hvac_mode:
103+
case HVACMode.OFF:
104+
await self._device.set_active(active=False)
105+
case HVACMode.AUTO:
106+
await self._device.set_active(active=True)
107+
except EheimDigitalClientError as err:
108+
raise HomeAssistantError from err
109+
110+
def _async_update_attrs(self) -> None:
111+
if self._device.temperature_unit == HeaterUnit.CELSIUS:
112+
self._attr_min_temp = 18
113+
self._attr_max_temp = 32
114+
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
115+
elif self._device.temperature_unit == HeaterUnit.FAHRENHEIT:
116+
self._attr_min_temp = 64
117+
self._attr_max_temp = 90
118+
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
119+
120+
self._attr_current_temperature = self._device.current_temperature
121+
self._attr_target_temperature = self._device.target_temperature
122+
123+
if self._device.is_heating:
124+
self._attr_hvac_action = HVACAction.HEATING
125+
self._attr_hvac_mode = HVACMode.AUTO
126+
elif self._device.is_active:
127+
self._attr_hvac_action = HVACAction.IDLE
128+
self._attr_hvac_mode = HVACMode.AUTO
129+
else:
130+
self._attr_hvac_action = HVACAction.OFF
131+
self._attr_hvac_mode = HVACMode.OFF
132+
133+
match self._device.operation_mode:
134+
case HeaterMode.MANUAL:
135+
self._attr_preset_mode = PRESET_NONE
136+
case HeaterMode.BIO:
137+
self._attr_preset_mode = HEATER_BIO_MODE
138+
case HeaterMode.SMART:
139+
self._attr_preset_mode = HEATER_SMART_MODE

‎homeassistant/components/eheimdigital/const.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from logging import Logger, getLogger
44

5-
from eheimdigital.types import LightMode
5+
from eheimdigital.types import HeaterMode, LightMode
66

7+
from homeassistant.components.climate import PRESET_NONE
78
from homeassistant.components.light import EFFECT_OFF
89

910
LOGGER: Logger = getLogger(__package__)
@@ -15,3 +16,12 @@
1516
EFFECT_DAYCL_MODE: LightMode.DAYCL_MODE,
1617
EFFECT_OFF: LightMode.MAN_MODE,
1718
}
19+
20+
HEATER_BIO_MODE = "bio_mode"
21+
HEATER_SMART_MODE = "smart_mode"
22+
23+
HEATER_PRESET_TO_HEATER_MODE = {
24+
HEATER_BIO_MODE: HeaterMode.BIO,
25+
HEATER_SMART_MODE: HeaterMode.SMART,
26+
PRESET_NONE: HeaterMode.MANUAL,
27+
}

‎homeassistant/components/eheimdigital/strings.json‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
}
2424
},
2525
"entity": {
26+
"climate": {
27+
"heater": {
28+
"state_attributes": {
29+
"preset_mode": {
30+
"state": {
31+
"bio_mode": "Bio mode",
32+
"smart_mode": "Smart mode"
33+
}
34+
}
35+
}
36+
}
37+
},
2638
"light": {
2739
"channel": {
2840
"name": "Channel {channel_id}",

‎tests/components/eheimdigital/conftest.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from unittest.mock import AsyncMock, MagicMock, patch
55

66
from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl
7+
from eheimdigital.heater import EheimDigitalHeater
78
from eheimdigital.hub import EheimDigitalHub
8-
from eheimdigital.types import EheimDeviceType, LightMode
9+
from eheimdigital.types import EheimDeviceType, HeaterMode, HeaterUnit, LightMode
910
import pytest
1011

1112
from homeassistant.components.eheimdigital.const import DOMAIN
@@ -39,7 +40,26 @@ def classic_led_ctrl_mock():
3940

4041

4142
@pytest.fixture
42-
def eheimdigital_hub_mock(classic_led_ctrl_mock: MagicMock) -> Generator[AsyncMock]:
43+
def heater_mock():
44+
"""Mock a Heater device."""
45+
heater_mock = MagicMock(spec=EheimDigitalHeater)
46+
heater_mock.mac_address = "00:00:00:00:00:02"
47+
heater_mock.device_type = EheimDeviceType.VERSION_EHEIM_EXT_HEATER
48+
heater_mock.name = "Mock Heater"
49+
heater_mock.aquarium_name = "Mock Aquarium"
50+
heater_mock.temperature_unit = HeaterUnit.CELSIUS
51+
heater_mock.current_temperature = 24.2
52+
heater_mock.target_temperature = 25.5
53+
heater_mock.is_heating = True
54+
heater_mock.is_active = True
55+
heater_mock.operation_mode = HeaterMode.MANUAL
56+
return heater_mock
57+
58+
59+
@pytest.fixture
60+
def eheimdigital_hub_mock(
61+
classic_led_ctrl_mock: MagicMock, heater_mock: MagicMock
62+
) -> Generator[AsyncMock]:
4363
"""Mock eheimdigital hub."""
4464
with (
4565
patch(
@@ -52,7 +72,8 @@ def eheimdigital_hub_mock(classic_led_ctrl_mock: MagicMock) -> Generator[AsyncMo
5272
),
5373
):
5474
eheimdigital_hub_mock.return_value.devices = {
55-
"00:00:00:00:00:01": classic_led_ctrl_mock
75+
"00:00:00:00:00:01": classic_led_ctrl_mock,
76+
"00:00:00:00:00:02": heater_mock,
5677
}
5778
eheimdigital_hub_mock.return_value.main = classic_led_ctrl_mock
5879
yield eheimdigital_hub_mock
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# serializer version: 1
2+
# name: test_setup_heater[climate.mock_heater_none-entry]
3+
EntityRegistryEntrySnapshot({
4+
'aliases': set({
5+
}),
6+
'area_id': None,
7+
'capabilities': dict({
8+
'hvac_modes': list([
9+
<HVACMode.OFF: 'off'>,
10+
<HVACMode.AUTO: 'auto'>,
11+
]),
12+
'max_temp': 32,
13+
'min_temp': 18,
14+
'preset_modes': list([
15+
'none',
16+
'bio_mode',
17+
'smart_mode',
18+
]),
19+
'target_temp_step': 0.5,
20+
}),
21+
'config_entry_id': <ANY>,
22+
'device_class': None,
23+
'device_id': <ANY>,
24+
'disabled_by': None,
25+
'domain': 'climate',
26+
'entity_category': None,
27+
'entity_id': 'climate.mock_heater_none',
28+
'has_entity_name': True,
29+
'hidden_by': None,
30+
'icon': None,
31+
'id': <ANY>,
32+
'labels': set({
33+
}),
34+
'name': None,
35+
'options': dict({
36+
}),
37+
'original_device_class': None,
38+
'original_icon': None,
39+
'original_name': None,
40+
'platform': 'eheimdigital',
41+
'previous_unique_id': None,
42+
'supported_features': <ClimateEntityFeature: 401>,
43+
'translation_key': 'heater',
44+
'unique_id': '00:00:00:00:00:02',
45+
'unit_of_measurement': None,
46+
})
47+
# ---
48+
# name: test_setup_heater[climate.mock_heater_none-state]
49+
StateSnapshot({
50+
'attributes': ReadOnlyDict({
51+
'current_temperature': 24.2,
52+
'friendly_name': 'Mock Heater None',
53+
'hvac_action': <HVACAction.HEATING: 'heating'>,
54+
'hvac_modes': list([
55+
<HVACMode.OFF: 'off'>,
56+
<HVACMode.AUTO: 'auto'>,
57+
]),
58+
'max_temp': 32,
59+
'min_temp': 18,
60+
'preset_mode': 'none',
61+
'preset_modes': list([
62+
'none',
63+
'bio_mode',
64+
'smart_mode',
65+
]),
66+
'supported_features': <ClimateEntityFeature: 401>,
67+
'target_temp_step': 0.5,
68+
'temperature': 25.5,
69+
}),
70+
'context': <ANY>,
71+
'entity_id': 'climate.mock_heater_none',
72+
'last_changed': <ANY>,
73+
'last_reported': <ANY>,
74+
'last_updated': <ANY>,
75+
'state': 'auto',
76+
})
77+
# ---

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /