Skip to content

Commit 2c34561

Browse files
authored
Add humidifier support for switchbot cloud integration (home-assistant#149039)
1 parent 1ef9018 commit 2c34561

File tree

10 files changed

+628
-1
lines changed

10 files changed

+628
-1
lines changed

homeassistant/components/switchbot_cloud/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Platform.CLIMATE,
3232
Platform.COVER,
3333
Platform.FAN,
34+
Platform.HUMIDIFIER,
3435
Platform.LIGHT,
3536
Platform.LOCK,
3637
Platform.SENSOR,
@@ -57,6 +58,7 @@ class SwitchbotDevices:
5758
locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
5859
fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
5960
lights: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
61+
humidifiers: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
6062

6163

6264
@dataclass
@@ -255,6 +257,19 @@ async def make_device_data(
255257
)
256258
devices_data.lights.append((device, coordinator))
257259

260+
if isinstance(device, Device) and device.device_type == "Humidifier2":
261+
coordinator = await coordinator_for_device(
262+
hass, entry, api, device, coordinators_by_id
263+
)
264+
devices_data.humidifiers.append((device, coordinator))
265+
266+
if isinstance(device, Device) and device.device_type == "Humidifier":
267+
coordinator = await coordinator_for_device(
268+
hass, entry, api, device, coordinators_by_id
269+
)
270+
devices_data.humidifiers.append((device, coordinator))
271+
devices_data.sensors.append((device, coordinator))
272+
258273

259274
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
260275
"""Set up SwitchBot via API from a config entry."""

homeassistant/components/switchbot_cloud/const.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
AFTER_COMMAND_REFRESH = 5
2121
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
2222

23+
HUMIDITY_LEVELS = {
24+
34: 101, # Low humidity mode
25+
67: 102, # Medium humidity mode
26+
100: 103, # High humidity mode
27+
}
28+
2329

2430
class AirPurifierMode(Enum):
2531
"""Air Purifier Modes."""
@@ -33,3 +39,21 @@ class AirPurifierMode(Enum):
3339
def get_modes(cls) -> list[str]:
3440
"""Return a list of available air purifier modes as lowercase strings."""
3541
return [mode.name.lower() for mode in cls]
42+
43+
44+
class Humidifier2Mode(Enum):
45+
"""Enumerates the available modes for a SwitchBot humidifier2."""
46+
47+
HIGH = 1
48+
MEDIUM = 2
49+
LOW = 3
50+
QUIET = 4
51+
TARGET_HUMIDITY = 5
52+
SLEEP = 6
53+
AUTO = 7
54+
DRYING_FILTER = 8
55+
56+
@classmethod
57+
def get_modes(cls) -> list[str]:
58+
"""Return a list of available humidifier2 modes as lowercase strings."""
59+
return [mode.name.lower() for mode in cls]
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""Support for Switchbot humidifier."""
2+
3+
import asyncio
4+
from typing import Any
5+
6+
from switchbot_api import CommonCommands, HumidifierCommands, HumidifierV2Commands
7+
8+
from homeassistant.components.humidifier import (
9+
MODE_AUTO,
10+
MODE_NORMAL,
11+
HumidifierDeviceClass,
12+
HumidifierEntity,
13+
HumidifierEntityFeature,
14+
)
15+
from homeassistant.config_entries import ConfigEntry
16+
from homeassistant.const import STATE_ON
17+
from homeassistant.core import HomeAssistant
18+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
19+
20+
from . import SwitchbotCloudData
21+
from .const import AFTER_COMMAND_REFRESH, DOMAIN, HUMIDITY_LEVELS, Humidifier2Mode
22+
from .entity import SwitchBotCloudEntity
23+
24+
PARALLEL_UPDATES = 0
25+
26+
27+
async def async_setup_entry(
28+
hass: HomeAssistant,
29+
entry: ConfigEntry,
30+
async_add_entities: AddConfigEntryEntitiesCallback,
31+
) -> None:
32+
"""Set up Switchbot based on a config entry."""
33+
data: SwitchbotCloudData = hass.data[DOMAIN][entry.entry_id]
34+
async_add_entities(
35+
SwitchBotHumidifier(data.api, device, coordinator)
36+
if device.device_type == "Humidifier"
37+
else SwitchBotEvaporativeHumidifier(data.api, device, coordinator)
38+
for device, coordinator in data.devices.humidifiers
39+
)
40+
41+
42+
class SwitchBotHumidifier(SwitchBotCloudEntity, HumidifierEntity):
43+
"""Representation of a Switchbot humidifier."""
44+
45+
_attr_supported_features = HumidifierEntityFeature.MODES
46+
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
47+
_attr_available_modes = [MODE_NORMAL, MODE_AUTO]
48+
_attr_min_humidity = 1
49+
_attr_translation_key = "humidifier"
50+
_attr_name = None
51+
_attr_target_humidity = 50
52+
53+
def _set_attributes(self) -> None:
54+
"""Set attributes from coordinator data."""
55+
if coord_data := self.coordinator.data:
56+
self._attr_is_on = coord_data.get("power") == STATE_ON
57+
self._attr_mode = MODE_AUTO if coord_data.get("auto") else MODE_NORMAL
58+
self._attr_current_humidity = coord_data.get("humidity")
59+
60+
async def async_set_humidity(self, humidity: int) -> None:
61+
"""Set new target humidity."""
62+
self.target_humidity, parameters = self._map_humidity_to_supported_level(
63+
humidity
64+
)
65+
await self.send_api_command(
66+
HumidifierCommands.SET_MODE, parameters=str(parameters)
67+
)
68+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
69+
await self.coordinator.async_request_refresh()
70+
71+
async def async_set_mode(self, mode: str) -> None:
72+
"""Set new target humidity."""
73+
if mode == MODE_AUTO:
74+
await self.send_api_command(HumidifierCommands.SET_MODE, parameters=mode)
75+
else:
76+
await self.send_api_command(
77+
HumidifierCommands.SET_MODE, parameters=str(102)
78+
)
79+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
80+
await self.coordinator.async_request_refresh()
81+
82+
async def async_turn_on(self, **kwargs: Any) -> None:
83+
"""Turn the device on."""
84+
await self.send_api_command(CommonCommands.ON)
85+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
86+
await self.coordinator.async_request_refresh()
87+
88+
async def async_turn_off(self, **kwargs: Any) -> None:
89+
"""Turn the device off."""
90+
await self.send_api_command(CommonCommands.OFF)
91+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
92+
await self.coordinator.async_request_refresh()
93+
94+
def _map_humidity_to_supported_level(self, humidity: int) -> tuple[int, int]:
95+
"""Map any humidity to the closest supported level and its parameter."""
96+
if humidity <= 34:
97+
return 34, HUMIDITY_LEVELS[34]
98+
if humidity <= 67:
99+
return 67, HUMIDITY_LEVELS[67]
100+
return 100, HUMIDITY_LEVELS[100]
101+
102+
103+
class SwitchBotEvaporativeHumidifier(SwitchBotCloudEntity, HumidifierEntity):
104+
"""Representation of a Switchbot humidifier v2."""
105+
106+
_attr_supported_features = HumidifierEntityFeature.MODES
107+
_attr_device_class = HumidifierDeviceClass.HUMIDIFIER
108+
_attr_available_modes = Humidifier2Mode.get_modes()
109+
_attr_translation_key = "evaporative_humidifier"
110+
_attr_name = None
111+
_attr_target_humidity = 50
112+
113+
def _set_attributes(self) -> None:
114+
"""Set attributes from coordinator data."""
115+
if coord_data := self.coordinator.data:
116+
self._attr_is_on = coord_data.get("power") == STATE_ON
117+
self._attr_mode = (
118+
Humidifier2Mode(coord_data.get("mode")).name.lower()
119+
if coord_data.get("mode") is not None
120+
else None
121+
)
122+
self._attr_current_humidity = (
123+
coord_data.get("humidity")
124+
if coord_data.get("humidity") != 127
125+
else None
126+
)
127+
128+
async def async_set_humidity(self, humidity: int) -> None:
129+
"""Set new target humidity."""
130+
assert self.coordinator.data is not None
131+
self._attr_target_humidity = humidity
132+
params = {"mode": self.coordinator.data["mode"], "humidity": humidity}
133+
await self.send_api_command(HumidifierV2Commands.SET_MODE, parameters=params)
134+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
135+
await self.coordinator.async_request_refresh()
136+
137+
async def async_set_mode(self, mode: str) -> None:
138+
"""Set new target mode."""
139+
assert self.coordinator.data is not None
140+
params = {"mode": Humidifier2Mode[mode.upper()].value}
141+
await self.send_api_command(HumidifierV2Commands.SET_MODE, parameters=params)
142+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
143+
await self.coordinator.async_request_refresh()
144+
145+
async def async_turn_on(self, **kwargs: Any) -> None:
146+
"""Turn the device on."""
147+
await self.send_api_command(CommonCommands.ON)
148+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
149+
await self.coordinator.async_request_refresh()
150+
151+
async def async_turn_off(self, **kwargs: Any) -> None:
152+
"""Turn the device off."""
153+
await self.send_api_command(CommonCommands.OFF)
154+
await asyncio.sleep(AFTER_COMMAND_REFRESH)
155+
await self.coordinator.async_request_refresh()

homeassistant/components/switchbot_cloud/icons.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@
3434
"10": "mdi:brightness-7"
3535
}
3636
}
37+
},
38+
"humidifier": {
39+
"evaporative_humidifier": {
40+
"state_attributes": {
41+
"mode": {
42+
"state": {
43+
"high": "mdi:water-plus",
44+
"medium": "mdi:water",
45+
"low": "mdi:water-outline",
46+
"quiet": "mdi:volume-off",
47+
"target_humidity": "mdi:target",
48+
"drying_filter": "mdi:water-remove"
49+
}
50+
}
51+
}
52+
}
3753
}
3854
}
3955
}

homeassistant/components/switchbot_cloud/sensor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@
160160
"Motion Sensor": (BATTERY_DESCRIPTION,),
161161
"Contact Sensor": (BATTERY_DESCRIPTION,),
162162
"Water Detector": (BATTERY_DESCRIPTION,),
163+
"Humidifier": (TEMPERATURE_DESCRIPTION,),
163164
}
164165

165166

homeassistant/components/switchbot_cloud/strings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@
3636
"light_level": {
3737
"name": "Light level"
3838
}
39+
},
40+
"humidifier": {
41+
"evaporative_humidifier": {
42+
"state_attributes": {
43+
"mode": {
44+
"state": {
45+
"high": "[%key:common::state::high%]",
46+
"medium": "[%key:common::state::medium%]",
47+
"low": "[%key:common::state::low%]",
48+
"quiet": "Quiet",
49+
"target_humidity": "Target humidity",
50+
"drying_filter": "Drying filter"
51+
}
52+
}
53+
}
54+
}
3955
}
4056
}
4157
}

tests/components/switchbot_cloud/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ async def configure_integration(hass: HomeAssistant) -> MockConfigEntry:
4141
hubDeviceId="test-hub-id",
4242
)
4343

44-
4544
METER_INFO = Device(
4645
version="V1.0",
4746
deviceId="meter-id-1",
@@ -81,3 +80,19 @@ async def configure_integration(hass: HomeAssistant) -> MockConfigEntry:
8180
deviceType="Water Detector",
8281
hubDeviceId="test-hub-id",
8382
)
83+
84+
HUMIDIFIER_INFO = Device(
85+
version="V1.0",
86+
deviceId="humidifier-id-1",
87+
deviceName="humidifier-1",
88+
deviceType="Humidifier",
89+
hubDeviceId="test-hub-id",
90+
)
91+
92+
HUMIDIFIER2_INFO = Device(
93+
version="V1.0",
94+
deviceId="humidifier2-id-1",
95+
deviceName="humidifier2-1",
96+
deviceType="Humidifier2",
97+
hubDeviceId="test-hub-id",
98+
)

tests/components/switchbot_cloud/fixtures/status.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,24 @@
4444
"deviceId": "water-detector-id",
4545
"deviceType": "Water Detector",
4646
"hubDeviceId": "test-hub-id"
47+
},
48+
{
49+
"version": "V1.8",
50+
"power": "on",
51+
"auto": false,
52+
"humidity": 50,
53+
"temperature": 24.3,
54+
"deviceId": "test-id-1",
55+
"deviceType": "Humidifier",
56+
"hubDeviceId": "test-hub-id"
57+
},
58+
{
59+
"version": "V1.0",
60+
"power": "on",
61+
"mode": 1,
62+
"humidity": 50,
63+
"deviceId": "test-id-1",
64+
"deviceType": "Humidifier2",
65+
"hubDeviceId": "test-hub-id"
4766
}
4867
]

0 commit comments

Comments
 (0)