File: evaporative_humidifier.py

package info (click to toggle)
pyswitchbot 0.72.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 876 kB
  • sloc: python: 12,717; makefile: 2
file content (249 lines) | stat: -rw-r--r-- 8,984 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import logging
from typing import Any

from bleak.backends.device import BLEDevice

from ..adv_parsers.humidifier import calculate_temperature_and_humidity
from ..const import SwitchbotModel
from ..const.evaporative_humidifier import (
    TARGET_HUMIDITY_MODES,
    HumidifierAction,
    HumidifierMode,
    HumidifierWaterLevel,
)
from .device import (
    SwitchbotEncryptedDevice,
    SwitchbotOperationError,
    SwitchbotSequenceDevice,
    update_after_operation,
)

_LOGGER = logging.getLogger(__name__)

COMMAND_HEADER = "57"
COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
COMMAND_TURN_ON = f"{COMMAND_HEADER}0f430101"
COMMAND_CHILD_LOCK_ON = f"{COMMAND_HEADER}0f430501"
COMMAND_CHILD_LOCK_OFF = f"{COMMAND_HEADER}0f430500"
COMMAND_AUTO_DRY_ON = f"{COMMAND_HEADER}0f430a01"
COMMAND_AUTO_DRY_OFF = f"{COMMAND_HEADER}0f430a02"
COMMAND_SET_MODE = f"{COMMAND_HEADER}0f4302"
COMMAND_GET_BASIC_INFO = f"{COMMAND_HEADER}000300"
COMMAND_SET_DRYING_FILTER = f"{COMMAND_TURN_ON}08"

MODES_COMMANDS = {
    HumidifierMode.HIGH: "010100",
    HumidifierMode.MEDIUM: "010200",
    HumidifierMode.LOW: "010300",
    HumidifierMode.QUIET: "010400",
    HumidifierMode.TARGET_HUMIDITY: "0200",
    HumidifierMode.SLEEP: "0300",
    HumidifierMode.AUTO: "040000",
}

DEVICE_GET_BASIC_SETTINGS_KEY = "570f4481"


class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
    """Representation of a Switchbot Evaporative Humidifier"""

    _turn_on_command = COMMAND_TURN_ON
    _turn_off_command = f"{COMMAND_HEADER}0f430100"

    def __init__(
        self,
        device: BLEDevice,
        key_id: str,
        encryption_key: str,
        interface: int = 0,
        model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
        **kwargs: Any,
    ) -> None:
        self._force_next_update = False
        super().__init__(device, key_id, encryption_key, model, interface, **kwargs)

    @classmethod
    async def verify_encryption_key(
        cls,
        device: BLEDevice,
        key_id: str,
        encryption_key: str,
        model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
        **kwargs: Any,
    ) -> bool:
        return await super().verify_encryption_key(
            device, key_id, encryption_key, model, **kwargs
        )

    async def get_basic_info(self) -> dict[str, Any] | None:
        """Get device basic settings."""
        if not (_data := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
            return None

        _LOGGER.debug("basic info data: %s", _data.hex())
        isOn = bool(_data[1] & 0b10000000)
        mode = HumidifierMode(_data[1] & 0b00001111)
        over_humidify_protection = bool(_data[2] & 0b10000000)
        child_lock = bool(_data[2] & 0b00100000)
        tank_removed = bool(_data[2] & 0b00000100)
        tilted_alert = bool(_data[2] & 0b00000010)
        filter_missing = bool(_data[2] & 0b00000001)
        is_meter_binded = bool(_data[3] & 0b10000000)

        _temp_c, _temp_f, humidity = calculate_temperature_and_humidity(
            _data[3:6], is_meter_binded
        )

        water_level = HumidifierWaterLevel(_data[5] & 0b00000011).name.lower()
        filter_run_time = int.from_bytes(_data[6:8], byteorder="big") & 0xFFF
        target_humidity = _data[10] & 0b01111111

        return {
            "isOn": isOn,
            "mode": mode,
            "over_humidify_protection": over_humidify_protection,
            "child_lock": child_lock,
            "tank_removed": tank_removed,
            "tilted_alert": tilted_alert,
            "filter_missing": filter_missing,
            "is_meter_binded": is_meter_binded,
            "humidity": humidity,
            "temperature": _temp_c,
            "temp": {"c": _temp_c, "f": _temp_f},
            "water_level": water_level,
            "filter_run_time": filter_run_time,
            "target_humidity": target_humidity,
        }

    @update_after_operation
    async def set_target_humidity(self, target_humidity: int) -> bool:
        """Set target humidity."""
        self._validate_water_level()
        self._validate_mode_for_target_humidity()
        command = (
            COMMAND_SET_MODE
            + MODES_COMMANDS[self.get_mode()]
            + f"{target_humidity:02x}"
        )
        result = await self._send_command(command)
        return self._check_command_result(result, 0, {1})

    @update_after_operation
    async def set_mode(self, mode: HumidifierMode) -> bool:
        """Set device mode."""
        self._validate_water_level()
        self._validate_meter_binding(mode)

        if mode == HumidifierMode.DRYING_FILTER:
            command = COMMAND_SET_DRYING_FILTER
        else:
            command = COMMAND_SET_MODE + MODES_COMMANDS[mode]

        if mode in TARGET_HUMIDITY_MODES:
            target_humidity = self.get_target_humidity()
            if target_humidity is None:
                raise SwitchbotOperationError(
                    "Target humidity must be set before switching to target humidity mode or sleep mode"
                )
            command += f"{target_humidity:02x}"
        result = await self._send_command(command)
        return self._check_command_result(result, 0, {1})

    def _validate_water_level(self, mode: HumidifierMode | None = None) -> None:
        """Validate that the water level is not empty."""
        if mode == HumidifierMode.DRYING_FILTER:
            return

        if self.get_water_level() == HumidifierWaterLevel.EMPTY.name.lower():
            raise SwitchbotOperationError(
                "Cannot perform operation when water tank is empty"
            )

    def _validate_mode_for_target_humidity(self) -> None:
        """Validate that the current mode supports target humidity."""
        if self.get_mode() not in TARGET_HUMIDITY_MODES:
            raise SwitchbotOperationError(
                "Target humidity can only be set in target humidity mode or sleep mode"
            )

    def _validate_meter_binding(self, mode: HumidifierMode) -> None:
        """Validate that the meter is binded for specific modes."""
        if not self.is_meter_binded() and mode in [
            HumidifierMode.TARGET_HUMIDITY,
            HumidifierMode.AUTO,
        ]:
            raise SwitchbotOperationError(
                "Cannot set target humidity or auto mode when meter is not binded"
            )

    @update_after_operation
    async def set_child_lock(self, enabled: bool) -> bool:
        """Set child lock."""
        result = await self._send_command(
            COMMAND_CHILD_LOCK_ON if enabled else COMMAND_CHILD_LOCK_OFF
        )
        return self._check_command_result(result, 0, {1})

    def is_on(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("isOn")

    def get_mode(self) -> HumidifierMode | None:
        """Return state from cache."""
        return self._get_adv_value("mode")

    def is_child_lock_enabled(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("child_lock")

    def is_over_humidify_protection_enabled(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("over_humidify_protection")

    def is_tank_removed(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("tank_removed")

    def is_filter_missing(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("filter_missing")

    def is_filter_alert_on(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("filter_alert")

    def is_tilted_alert_on(self) -> bool | None:
        """Return state from cache."""
        return self._get_adv_value("tilted_alert")

    def get_water_level(self) -> HumidifierWaterLevel | None:
        """Return state from cache."""
        return self._get_adv_value("water_level")

    def get_filter_run_time(self) -> int | None:
        """Return state from cache."""
        return self._get_adv_value("filter_run_time")

    def get_target_humidity(self) -> int | None:
        """Return state from cache."""
        return self._get_adv_value("target_humidity")

    def get_humidity(self) -> int | None:
        """Return state from cache."""
        return self._get_adv_value("humidity")

    def get_temperature(self) -> float | None:
        """Return state from cache."""
        return self._get_adv_value("temperature")

    def get_action(self) -> int:
        """Return current action from cache."""
        if not self.is_on():
            return HumidifierAction.OFF
        if self.get_mode() != HumidifierMode.DRYING_FILTER:
            return HumidifierAction.HUMIDIFYING
        return HumidifierAction.DRYING

    def is_meter_binded(self) -> bool | None:
        """Return meter bind state from cache."""
        return self._get_adv_value("is_meter_binded")