Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for zhimi.heater.za2 #1301

Merged
merged 8 commits into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Supported devices
- Xiaomi Mi Smart Space Heater
- Xiaomiyoupin Curtain Controller (Wi-Fi) (lumi.curtain.hagl05)
- Xiaomi Xiaomi Mi Smart Space Heater S (zhimi.heater.mc2)
- Xiaomi Xiaomi Mi Smart Space Heater 1S (zhimi.heater.za2)
- Yeelight Dual Control Module (yeelink.switch.sw1)
- Scishare coffee maker (scishare.coffee.s1102)
- Qingping Air Monitor Lite (cgllc.airm.cgdn1)
Expand Down
102 changes: 75 additions & 27 deletions miio/heater_miot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import enum
import logging
from typing import Any, Dict
from typing import Any, Dict, Optional

import click

Expand All @@ -9,42 +9,70 @@
from .miot_device import DeviceStatus, MiotDevice

_LOGGER = logging.getLogger(__name__)
_MAPPING = {
# Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:heater:0000A01A:zhimi-mc2:1
# Heater (siid=2)
"power": {"siid": 2, "piid": 1},
"target_temperature": {"siid": 2, "piid": 5},
# Countdown (siid=3)
"countdown_time": {"siid": 3, "piid": 1},
# Environment (siid=4)
"temperature": {"siid": 4, "piid": 7},
# Physical Control Locked (siid=6)
"child_lock": {"siid": 5, "piid": 1},
# Alarm (siid=6)
"buzzer": {"siid": 6, "piid": 1},
# Indicator light (siid=7)
"led_brightness": {"siid": 7, "piid": 3},
_MAPPINGS = {
"zhimi.heater.mc2": {
# Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:heater:0000A01A:zhimi-mc2:1
# Heater (siid=2)
"power": {"siid": 2, "piid": 1},
"target_temperature": {"siid": 2, "piid": 5},
# Countdown (siid=3)
"countdown_time": {"siid": 3, "piid": 1},
# Environment (siid=4)
"temperature": {"siid": 4, "piid": 7},
# Physical Control Locked (siid=5)
"child_lock": {"siid": 5, "piid": 1},
# Alarm (siid=6)
"buzzer": {"siid": 6, "piid": 1},
# Indicator light (siid=7)
"led_brightness": {"siid": 7, "piid": 3},
},
"zhimi.heater.za2": {
# Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:heater:0000A01A:zhimi-za2:1
# Heater (siid=2)
"power": {"siid": 2, "piid": 2},
"target_temperature": {"siid": 2, "piid": 6},
# Countdown (siid=4)
"countdown_time": {"siid": 4, "piid": 1},
# Environment (siid=5)
"temperature": {"siid": 5, "piid": 8},
"relative_humidity": {"siid": 5, "piid": 7},
# Physical Control Locked (siid=7)
"child_lock": {"siid": 7, "piid": 1},
# Alarm (siid=3)
"buzzer": {"siid": 3, "piid": 1},
# Indicator light (siid=7)
"led_brightness": {"siid": 6, "piid": 1},
},
}

HEATER_PROPERTIES = {
"temperature_range": (18, 28),
"delay_off_range": (0, 12 * 3600),
"zhimi.heater.mc2": {
"temperature_range": (18, 28),
"delay_off_range": (0, 12 * 3600),
},
"zhimi.heater.za2": {
"temperature_range": (16, 28),
"delay_off_range": (0, 8 * 3600),
},
}


class LedBrightness(enum.Enum):
"""Note that only Xiaomi Smart Space Heater 1S (zhimi.heater.za2) supports `Dim`."""

On = 0
Off = 1
Dim = 2


class HeaterMiotException(DeviceException):
pass


class HeaterMiotStatus(DeviceStatus):
"""Container for status reports from the Xiaomi Smart Space Heater S."""
"""Container for status reports from the Xiaomi Smart Space Heater S and 1S."""

def __init__(self, data: Dict[str, Any]) -> None:
def __init__(self, data: Dict[str, Any], model: str) -> None:
"""
Response (MIoT format) of Xiaomi Smart Space Heater S (zhimi.heater.mc2):

Expand All @@ -59,6 +87,7 @@ def __init__(self, data: Dict[str, Any]) -> None:
]
"""
self.data = data
self.model = model

@property
def power(self) -> str:
Expand All @@ -85,6 +114,11 @@ def temperature(self) -> float:
"""Current temperature."""
return self.data["temperature"]

@property
def relative_humidity(self) -> Optional[int]:
"""Current relative humidity."""
return self.data.get("relative_humidity")

@property
def child_lock(self) -> bool:
"""True if child lock is on, False otherwise."""
Expand All @@ -98,13 +132,17 @@ def buzzer(self) -> bool:
@property
def led_brightness(self) -> LedBrightness:
"""LED indicator brightness."""
return LedBrightness(self.data["led_brightness"])
value = self.data["led_brightness"]
if self.model == "zhimi.heater.za2" and value:
value = 3 - value
return LedBrightness(value)


class HeaterMiot(MiotDevice):
"""Main class representing the Xiaomi Smart Space Heater S (zhimi.heater.mc2)."""
"""Main class representing the Xiaomi Smart Space Heater S (zhimi.heater.mc2) & 1S
(zhimi.heater.za2)."""

mapping = _MAPPING
_mappings = _MAPPINGS

@command(
default_output=format_output(
Expand All @@ -125,7 +163,8 @@ def status(self) -> HeaterMiotStatus:
{
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties_for_mapping()
}
},
self.model,
)

@command(default_output=format_output("Powering on"))
Expand All @@ -146,7 +185,9 @@ def off(self):
)
def set_target_temperature(self, target_temperature: int):
"""Set target_temperature ."""
min_temp, max_temp = HEATER_PROPERTIES["temperature_range"]
min_temp, max_temp = HEATER_PROPERTIES.get(
self.model, {"temperature_range": (18, 28)}
)["temperature_range"]
if target_temperature < min_temp or target_temperature > max_temp:
raise HeaterMiotException(
"Invalid temperature: %s. Must be between %s and %s."
Expand Down Expand Up @@ -182,15 +223,22 @@ def set_buzzer(self, buzzer: bool):
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.set_property("led_brightness", brightness.value)
value = brightness.value
if self.model == "zhimi.heater.za2" and value:
value = 3 - value # Actually 1 means Dim, 2 means Off in za2
elif value == 2:
raise ValueError("Unsupported brightness Dim for model '%s'.", self.model)
return self.set_property("led_brightness", value)

@command(
click.argument("seconds", type=int),
default_output=format_output("Setting delayed turn off to {seconds} seconds"),
)
def set_delay_off(self, seconds: int):
"""Set delay off seconds."""
min_delay, max_delay = HEATER_PROPERTIES["delay_off_range"]
min_delay, max_delay = HEATER_PROPERTIES.get(
self.model, {"delay_off_range": (0, 12 * 3600)}
)["delay_off_range"]
if seconds < min_delay or seconds > max_delay:
raise HeaterMiotException(
"Invalid scheduled turn off: %s. Must be between %s and %s"
Expand Down