Skip to content

Commit

Permalink
Initial support for lumi.curtain.hagl05 (#851)
Browse files Browse the repository at this point in the history
* Added support for the Xiaomi Smart Curtain Motor (Wi-Fi version)

* module refactoring: Xiaomi Smart Curtain Motor

* lumi.curtain.hagl05 curtain: @rytilahti suggestions applied

* fixed parameter naming

* lumi.curtain.hagl05 curtain: removed unnecessary DeviceError processing

* lumi.curtain.hagl05 curtain: added hints for the ValueError

* lumi.curtain.hagl05 curtain: added boolean types for two-states parameters

* Apply suggestions from code review

lumi.curtain.hagl05 curtain: one-liner comments, returns for the setters

Co-authored-by: Teemu R. <tpr@iki.fi>

* lumi.curtain.hagl05: updated Readme, docstrings updated, returns for setters

* lumi.curtain.hagl05 curtain: refactoring with code checks

Co-authored-by: Teemu R. <tpr@iki.fi>
  • Loading branch information
in7egral and rytilahti authored Nov 6, 2020
1 parent 460a1d4 commit 006a349
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Supported devices
- Xiaomi Xiao AI Smart Alarm Clock
- Smartmi Radiant Heater Smart Version (ZA1 version)
- Xiaomi Mi Smart Space Heater
- Xiaomiyoupin Curtain Controller (Wi-Fi) (lumi.curtain.hagl05)


*Feel free to create a pull request to add support for new devices as
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from miio.chuangmi_ir import ChuangmiIr
from miio.chuangmi_plug import ChuangmiPlug, Plug, PlugV1, PlugV3
from miio.cooker import Cooker
from miio.curtain_youpin import CurtainMiot
from miio.device import Device
from miio.exceptions import DeviceError, DeviceException
from miio.fan import Fan, FanP5, FanSA1, FanV2, FanZA1, FanZA4
Expand Down
236 changes: 236 additions & 0 deletions miio/curtain_youpin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import enum
import logging
from typing import Any, Dict

import click

from .click_common import EnumType, command, format_output
from .miot_device import MiotDevice

_LOGGER = logging.getLogger(__name__)
_MAPPING = {
# # Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:curtain:0000A00C:lumi-hagl05:1
# Curtain
"motor_control": {"siid": 2, "piid": 2}, # 0 - Pause, 1 - Open, 2 - Close, 3 - auto
"current_position": {"siid": 2, "piid": 3}, # Range: [0, 100, 1]
"status": {"siid": 2, "piid": 6}, # 0 - Stopped, 1 - Opening, 2 - Closing
"target_position": {"siid": 2, "piid": 7}, # Range: [0, 100, 1]
# curtain_cfg
"manual_enabled": {"siid": 4, "piid": 1}, #
"polarity": {"siid": 4, "piid": 2},
"is_position_limited": {"siid": 4, "piid": 3},
"night_tip_light": {"siid": 4, "piid": 4},
"run_time": {"siid": 4, "piid": 5}, # Range: [0, 255, 1]
# motor_controller
"adjust_value": {"siid": 5, "piid": 1}, # Range: [-100, 100, 1]
}

# Model: ZNCLDJ21LM (also known as "Xiaomiyoupin Curtain Controller (Wi-Fi)"
MODEL_CURTAIN_HAGL05 = "lumi.curtain.hagl05"


class MotorControl(enum.Enum):
Pause = 0
Open = 1
Close = 2
Auto = 3


class Status(enum.Enum):
Stopped = 0
Opening = 1
Closing = 2


class Polarity(enum.Enum):
Positive = 0
Reverse = 1


class CurtainStatus:
def __init__(self, data: Dict[str, Any]) -> None:
"""Response from device
{'id': 1, 'result': [
{'did': 'current_position', 'siid': 2, 'piid': 3, 'code': 0, 'value': 0},
{'did': 'status', 'siid': 2, 'piid': 6, 'code': 0, 'value': 0},
{'did': 'target_position', 'siid': 2, 'piid': 7, 'code': 0, 'value': 0},
{'did': 'is_manual_enabled', 'siid': 4, 'piid': 1, 'code': 0, 'value': 1},
{'did': 'polarity', 'siid': 4, 'piid': 2, 'code': 0, 'value': 0},
{'did': 'is_position_limited', 'siid': 4, 'piid': 3, 'code': 0, 'value': 0},
{'did': 'night_tip_light', 'siid': 4, 'piid': 4, 'code': 0, 'value': 1},
{'did': 'run_time', 'siid': 4, 'piid': 5, 'code': 0, 'value': 0},
{'did': 'adjust_value', 'siid': 5, 'piid': 1, 'code': -4000}
]}
"""
self.data = data

@property
def status(self) -> Status:
"""Device status."""
return Status(self.data["status"])

@property
def is_manual_enabled(self) -> bool:
"""True if manual controls are enabled."""
return bool(self.data["is_manual_enabled"])

@property
def polarity(self) -> Polarity:
"""Motor rotation polarity."""
return Polarity(self.data["polarity"])

@property
def is_position_limited(self) -> bool:
"""Position limit."""
return bool(self.data["is_position_limited"])

@property
def night_tip_light(self) -> bool:
"""Night tip light status."""
return bool(self.data["night_tip_light"])

@property
def run_time(self) -> int:
"""Run time of the motor."""
return self.data["run_time"]

@property
def current_position(self) -> int:
"""Current curtain position."""
return self.data["current_position"]

@property
def target_position(self) -> int:
"""Target curtain position."""
return self.data["target_position"]

@property
def adjust_value(self) -> int:
""" Adjust value."""
return self.data["adjust_value"]

def __repr__(self) -> str:
s = (
"<CurtainStatus"
"status=%s,"
"polarity=%s,"
"is_position_limited=%s,"
"night_tip_light=%s,"
"run_time=%s,"
"current_position=%s,"
"target_position=%s,"
"adjust_value=%s>"
% (
self.status,
self.polarity,
self.is_position_limited,
self.night_tip_light,
self.run_time,
self.current_position,
self.target_position,
self.adjust_value,
)
)
return s


class CurtainMiot(MiotDevice):
"""Main class representing the lumi.curtain.hagl05 curtain."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)

@command(
default_output=format_output(
"",
"Device status: {result.status}\n"
"Manual enabled: {result.is_manual_enabled}\n"
"Motor polarity: {result.polarity}\n"
"Position limit: {result.is_position_limited}\n"
"Enabled night tip light: {result.night_tip_light}\n"
"Run time: {result.run_time}\n"
"Current position: {result.current_position}\n"
"Target position: {result.target_position}\n"
"Adjust value: {result.adjust_value}\n",
)
)
def status(self) -> CurtainStatus:
"""Retrieve properties."""

return CurtainStatus(
{
prop["did"]: prop["value"] if prop["code"] == 0 else None
for prop in self.get_properties_for_mapping()
}
)

@command(
click.argument("motor_control", type=EnumType(MotorControl)),
default_output=format_output("Set motor control to {motor_control}"),
)
def set_motor_control(self, motor_control: MotorControl):
"""Set motor control."""
return self.set_property("motor_control", motor_control.value)

@command(
click.argument("target_position", type=int),
default_output=format_output("Set target position to {target_position}"),
)
def set_target_position(self, target_position: int):
"""Set target position."""
if target_position < 0 or target_position > 100:
raise ValueError(
"Value must be between [0, 100] value, was %s" % target_position
)
return self.set_property("target_position", target_position)

@command(
click.argument("manual_enabled", type=bool),
default_output=format_output("Set manual control {manual_enabled}"),
)
def set_manual_enabled(self, manual_enabled: bool):
"""Set manual control of curtain."""
return self.set_property("is_manual_enabled", manual_enabled)

@command(
click.argument("polarity", type=EnumType(Polarity)),
default_output=format_output("Set polarity to {polarity}"),
)
def set_polarity(self, polarity: Polarity):
"""Set polarity of the motor."""
return self.set_property("polarity", polarity.value)

@command(
click.argument("pos_limit", type=bool),
default_output=format_output("Set position limit to {pos_limit}"),
)
def set_position_limit(self, pos_limit: bool):
"""Set position limit parameter."""
return self.set_property("is_position_limited", pos_limit)

@command(
click.argument("night_tip_light", type=bool),
default_output=format_output("Setting night tip light {night_tip_light"),
)
def set_night_tip_light(self, night_tip_light: bool):
"""Set night tip light."""
return self.set_property("night_tip_light", night_tip_light)

@command(
click.argument("adjust_value", type=int),
default_output=format_output("Set adjust value to {adjust_value}"),
)
def set_adjust_value(self, adjust_value: int):
"""Adjust to preferred position."""
if adjust_value < -100 or adjust_value > 100:
raise ValueError(
"Value must be between [-100, 100] value, was %s" % adjust_value
)
return self.set_property("adjust_value", adjust_value)

0 comments on commit 006a349

Please sign in to comment.