Skip to content

Commit b9135c4

Browse files
committed
Add support for water_heater entities.
1 parent 86d1b38 commit b9135c4

File tree

8 files changed

+299
-59
lines changed

8 files changed

+299
-59
lines changed

aioesphomeapi/api.proto

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ service APIConnection {
6363
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
6464

6565
rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
66+
67+
rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {}
6668
}
6769

6870

@@ -1899,3 +1901,64 @@ message UpdateCommandRequest {
18991901
fixed32 key = 1;
19001902
bool install = 2;
19011903
}
1904+
1905+
// ==================== WATER HEATER ====================
1906+
enum WaterHeaterMode {
1907+
WATER_HEATER_MODE_OFF = 0;
1908+
WATER_HEATER_MODE_ECO = 1;
1909+
WATER_HEATER_MODE_ELECTRIC = 2;
1910+
WATER_HEATER_MODE_PERFORMANCE = 3;
1911+
WATER_HEATER_MODE_HIGH_DEMAND = 4;
1912+
WATER_HEATER_MODE_HEAT_PUMP = 5;
1913+
WATER_HEATER_MODE_GAS = 6;
1914+
}
1915+
message ListEntitiesWaterHeaterResponse {
1916+
option (id) = 119;
1917+
option (source) = SOURCE_SERVER;
1918+
option (ifdef) = "USE_WATER_HEATER";
1919+
1920+
string object_id = 1;
1921+
fixed32 key = 2;
1922+
string name = 3;
1923+
string unique_id = 4;
1924+
1925+
bool supports_current_temperature = 5;
1926+
bool supports_two_point_target_temperature = 6;
1927+
repeated WaterHeaterMode supported_modes = 7;
1928+
float visual_min_temperature = 8;
1929+
float visual_max_temperature = 9;
1930+
float visual_target_temperature_step = 10;
1931+
bool disabled_by_default = 11;
1932+
string icon = 12;
1933+
EntityCategory entity_category = 13;
1934+
float visual_current_temperature_step = 14;
1935+
}
1936+
message WaterHeaterStateResponse {
1937+
option (id) = 120;
1938+
option (source) = SOURCE_SERVER;
1939+
option (ifdef) = "USE_WATER_HEATER";
1940+
option (no_delay) = true;
1941+
1942+
fixed32 key = 1;
1943+
WaterHeaterMode mode = 2;
1944+
float current_temperature = 3;
1945+
float target_temperature = 4;
1946+
float target_temperature_low = 5;
1947+
float target_temperature_high = 6;
1948+
}
1949+
message WaterHeaterCommandRequest {
1950+
option (id) = 121;
1951+
option (source) = SOURCE_CLIENT;
1952+
option (ifdef) = "USE_WATER_HEATER";
1953+
option (no_delay) = true;
1954+
1955+
fixed32 key = 1;
1956+
bool has_mode = 2;
1957+
WaterHeaterMode mode = 3;
1958+
bool has_target_temperature = 4;
1959+
float target_temperature = 5;
1960+
bool has_target_temperature_low = 6;
1961+
float target_temperature_low = 7;
1962+
bool has_target_temperature_high = 8;
1963+
float target_temperature_high = 9;
1964+
}

aioesphomeapi/api_pb2.py

Lines changed: 106 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aioesphomeapi/client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
VoiceAssistantRequest,
7777
VoiceAssistantResponse,
7878
VoiceAssistantTimerEventResponse,
79+
WaterHeaterCommandRequest,
7980
)
8081
from .client_callbacks import (
8182
on_bluetooth_connections_free_response,
@@ -134,6 +135,7 @@
134135
VoiceAssistantEventType,
135136
VoiceAssistantSubscriptionFlag,
136137
VoiceAssistantTimerEventType,
138+
WaterHeaterMode,
137139
message_types_to_names,
138140
)
139141
from .model_conversions import (
@@ -1423,3 +1425,27 @@ def alarm_control_panel_command(
14231425
if code is not None:
14241426
req.code = code
14251427
self._get_connection().send_message(req)
1428+
1429+
def water_heater_command(
1430+
self,
1431+
key: int,
1432+
mode: WaterHeaterMode | None = None,
1433+
target_temperature: float | None = None,
1434+
target_temperature_low: float | None = None,
1435+
target_temperature_high: float | None = None,
1436+
) -> None:
1437+
connection = self._get_connection()
1438+
req = WaterHeaterCommandRequest(key=key)
1439+
if mode is not None:
1440+
req.has_mode = True
1441+
req.mode = mode
1442+
if target_temperature is not None:
1443+
req.has_target_temperature = True
1444+
req.target_temperature = target_temperature
1445+
if target_temperature_low is not None:
1446+
req.has_target_temperature_low = True
1447+
req.target_temperature_low = target_temperature_low
1448+
if target_temperature_high is not None:
1449+
req.has_target_temperature_high = True
1450+
req.target_temperature_high = target_temperature_high
1451+
connection.send_message(req)

aioesphomeapi/core.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
ListEntitiesTimeResponse,
8585
ListEntitiesUpdateResponse,
8686
ListEntitiesValveResponse,
87+
ListEntitiesWaterHeaterResponse,
8788
LockCommandRequest,
8889
LockStateResponse,
8990
MediaPlayerCommandRequest,
@@ -123,6 +124,8 @@
123124
VoiceAssistantRequest,
124125
VoiceAssistantResponse,
125126
VoiceAssistantTimerEventResponse,
127+
WaterHeaterCommandRequest,
128+
WaterHeaterStateResponse,
126129
)
127130

128131
TWO_CHAR = re.compile(r".{2}")
@@ -392,4 +395,7 @@ def __init__(self, error: BluetoothGATTError) -> None:
392395
116: ListEntitiesUpdateResponse,
393396
117: UpdateStateResponse,
394397
118: UpdateCommandRequest,
398+
119: ListEntitiesWaterHeaterResponse,
399+
120: WaterHeaterStateResponse,
400+
121: WaterHeaterCommandRequest,
395401
}

aioesphomeapi/model.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,6 +1291,57 @@ class VoiceAssistantTimerEventType(APIIntEnum):
12911291
VOICE_ASSISTANT_TIMER_FINISHED = 3
12921292

12931293

1294+
# ==================== WATER HEATER ====================
1295+
class WaterHeaterMode(APIIntEnum):
1296+
OFF = 0
1297+
ECO = 1
1298+
ELECTRIC = 2
1299+
PERFORMANCE = 3
1300+
HIGH_DEMAND = 4
1301+
HEAT_PUMP = 5
1302+
GAS = 6
1303+
1304+
1305+
@_frozen_dataclass_decorator
1306+
class WaterHeaterInfo(EntityInfo):
1307+
supports_current_temperature: bool = False
1308+
supports_two_point_target_temperature: bool = False
1309+
supported_modes: list[WaterHeaterMode] = converter_field(
1310+
default_factory=list, converter=WaterHeaterMode.convert_list
1311+
)
1312+
visual_min_temperature: float = converter_field(
1313+
default=0.0, converter=fix_float_single_double_conversion
1314+
)
1315+
visual_max_temperature: float = converter_field(
1316+
default=0.0, converter=fix_float_single_double_conversion
1317+
)
1318+
visual_target_temperature_step: float = converter_field(
1319+
default=0.0, converter=fix_float_single_double_conversion
1320+
)
1321+
visual_current_temperature_step: float = converter_field(
1322+
default=0.0, converter=fix_float_single_double_conversion
1323+
)
1324+
1325+
1326+
@_frozen_dataclass_decorator
1327+
class WaterHeaterState(EntityState):
1328+
mode: WaterHeaterMode | None = converter_field(
1329+
default=WaterHeaterMode.OFF, converter=WaterHeaterMode.convert
1330+
)
1331+
current_temperature: float = converter_field(
1332+
default=0.0, converter=fix_float_single_double_conversion
1333+
)
1334+
target_temperature: float = converter_field(
1335+
default=0.0, converter=fix_float_single_double_conversion
1336+
)
1337+
target_temperature_low: float = converter_field(
1338+
default=0.0, converter=fix_float_single_double_conversion
1339+
)
1340+
target_temperature_high: float = converter_field(
1341+
default=0.0, converter=fix_float_single_double_conversion
1342+
)
1343+
1344+
12941345
_TYPE_TO_NAME = {
12951346
BinarySensorInfo: "binary_sensor",
12961347
ButtonInfo: "button",
@@ -1315,6 +1366,7 @@ class VoiceAssistantTimerEventType(APIIntEnum):
13151366
ValveInfo: "valve",
13161367
EventInfo: "event",
13171368
UpdateInfo: "update",
1369+
WaterHeaterInfo: "water_heater",
13181370
}
13191371

13201372

aioesphomeapi/model_conversions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ListEntitiesTimeResponse,
3737
ListEntitiesUpdateResponse,
3838
ListEntitiesValveResponse,
39+
ListEntitiesWaterHeaterResponse,
3940
LockStateResponse,
4041
MediaPlayerStateResponse,
4142
NumberStateResponse,
@@ -48,6 +49,7 @@
4849
TimeStateResponse,
4950
UpdateStateResponse,
5051
ValveStateResponse,
52+
WaterHeaterStateResponse,
5153
)
5254
from .model import (
5355
AlarmControlPanelEntityState,
@@ -96,6 +98,8 @@
9698
UpdateState,
9799
ValveInfo,
98100
ValveState,
101+
WaterHeaterInfo,
102+
WaterHeaterState,
99103
)
100104

101105
SUBSCRIBE_STATES_RESPONSE_TYPES: dict[Any, type[EntityState]] = {
@@ -120,6 +124,7 @@
120124
TimeStateResponse: TimeState,
121125
UpdateStateResponse: UpdateState,
122126
ValveStateResponse: ValveState,
127+
WaterHeaterStateResponse: WaterHeaterState,
123128
}
124129

125130
LIST_ENTITIES_SERVICES_RESPONSE_TYPES: dict[Any, type[EntityInfo] | None] = {
@@ -147,4 +152,5 @@
147152
ListEntitiesTimeResponse: TimeInfo,
148153
ListEntitiesUpdateResponse: UpdateInfo,
149154
ListEntitiesValveResponse: ValveInfo,
155+
ListEntitiesWaterHeaterResponse: WaterHeaterInfo,
150156
}

tests/test_client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
VoiceAssistantRequest,
7575
VoiceAssistantResponse,
7676
VoiceAssistantTimerEventResponse,
77+
WaterHeaterCommandRequest,
7778
)
7879
from aioesphomeapi.client import APIClient, BluetoothConnectionDroppedError
7980
from aioesphomeapi.connection import APIConnection
@@ -118,6 +119,7 @@
118119
from aioesphomeapi.model import (
119120
VoiceAssistantTimerEventType as VoiceAssistantTimerEventModelType,
120121
)
122+
from aioesphomeapi.model import WaterHeaterMode
121123
from aioesphomeapi.reconnect_logic import ReconnectLogic, ReconnectLogicState
122124

123125
from .common import (
@@ -2599,3 +2601,34 @@ async def test_calls_after_connection_closed(
25992601

26002602
with pytest.raises(APIConnectionError):
26012603
await client.update_command(1, True)
2604+
2605+
2606+
@pytest.mark.asyncio
2607+
@pytest.mark.parametrize(
2608+
"cmd, req",
2609+
[
2610+
(
2611+
dict(key=1, mode=WaterHeaterMode.HEAT_PUMP),
2612+
dict(key=1, has_mode=True, mode=WaterHeaterMode.HEAT_PUMP),
2613+
),
2614+
(
2615+
dict(key=1, target_temperature=21.0),
2616+
dict(key=1, has_target_temperature=True, target_temperature=21.0),
2617+
),
2618+
(
2619+
dict(key=1, target_temperature_low=21.0),
2620+
dict(key=1, has_target_temperature_low=True, target_temperature_low=21.0),
2621+
),
2622+
(
2623+
dict(key=1, target_temperature_high=21.0),
2624+
dict(key=1, has_target_temperature_high=True, target_temperature_high=21.0),
2625+
),
2626+
],
2627+
)
2628+
async def test_water_heater_command(
2629+
auth_client: APIClient, cmd: dict[str, Any], req: dict[str, Any]
2630+
) -> None:
2631+
send = patch_send(auth_client)
2632+
2633+
auth_client.water_heater_command(**cmd)
2634+
send.assert_called_once_with(WaterHeaterCommandRequest(**req))

tests/test_model.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
ListEntitiesTimeResponse,
4444
ListEntitiesUpdateResponse,
4545
ListEntitiesValveResponse,
46+
ListEntitiesWaterHeaterResponse,
4647
LockStateResponse,
4748
MediaPlayerStateResponse,
4849
NumberStateResponse,
@@ -55,6 +56,7 @@
5556
TimeStateResponse,
5657
UpdateStateResponse,
5758
ValveStateResponse,
59+
WaterHeaterStateResponse,
5860
)
5961
from aioesphomeapi.model import (
6062
_TYPE_TO_NAME,
@@ -122,6 +124,8 @@
122124
ValveInfo,
123125
ValveState,
124126
VoiceAssistantFeature,
127+
WaterHeaterInfo,
128+
WaterHeaterState,
125129
build_unique_id,
126130
converter_field,
127131
)
@@ -292,6 +296,8 @@ def test_api_version_ord():
292296
(Event, EventResponse),
293297
(UpdateInfo, ListEntitiesUpdateResponse),
294298
(UpdateState, UpdateStateResponse),
299+
(WaterHeaterInfo, ListEntitiesWaterHeaterResponse),
300+
(WaterHeaterState, WaterHeaterStateResponse),
295301
],
296302
)
297303
def test_basic_pb_conversions(model, pb):
@@ -409,6 +415,7 @@ def test_user_service_conversion():
409415
AlarmControlPanelInfo,
410416
TextInfo,
411417
TimeInfo,
418+
WaterHeaterInfo,
412419
],
413420
)
414421
def test_build_unique_id(model):

0 commit comments

Comments
 (0)