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

Initial support for lumi.curtain.hagl05 #851

Merged
merged 10 commits into from
Nov 6, 2020
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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
290 changes: 290 additions & 0 deletions miio/curtain_youpin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import enum
import warnings
import logging

import click

from typing import Any, Dict
from .click_common import EnumType, command, format_output
from .exceptions import DeviceException
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},
"position_limit": {"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 CurtainMiotException(DeviceException):
pass

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 ManualEnabled(enum.Enum):
Disable = 0
Enable = 1

class PosLimit(enum.Enum):
Unlimit = 0
Limit = 1

class NightTipLight(enum.Enum):
Disable = 0
Enable = 1


class CurtainStatus:
def __init__(self, data: Dict[str, Any]) -> None:
""" Response from device
{'id': 1, 'result': [
{'did': 'motor_control', 'siid': 2, 'piid': 2, 'code': -4001},
in7egral marked this conversation as resolved.
Show resolved Hide resolved
{'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': 'manual_enabled', 'siid': 4, 'piid': 1, 'code': 0, 'value': 1},
{'did': 'polarity', 'siid': 4, 'piid': 2, 'code': 0, 'value': 0},
{'did': 'position_limit', '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 """
in7egral marked this conversation as resolved.
Show resolved Hide resolved
return Status(self.data["status"])

""" curtain_cfg """
in7egral marked this conversation as resolved.
Show resolved Hide resolved
@property
def manual_enabled(self) -> ManualEnabled:
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""Manual control enable status
Values: Disable, Enable
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
return ManualEnabled(self.data["manual_enabled"])

@property
def polarity(self) -> Polarity:
"""Polarity
Values: Positive, Reverse
"""
return Polarity(self.data["polarity"])

@property
def position_limit(self) -> PosLimit:
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""Position limit
Values: Unlimit, Limit
"""
return PosLimit(self.data["position_limit"])

@property
def night_tip_light(self) -> NightTipLight:
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""Night tip light status
Values: Enable, Disable
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
return NightTipLight(self.data["night_tip_light"])

@property
def run_time(self) -> int:
"""Run time
Value range: [0, 255, 1]
"""
return self.data["run_time"]

@property
def current_position(self) -> int:
"""Current position
Value range: [0, 100, 1]
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
return self.data["current_position"]

@property
def target_position(self) -> int:
"""Target position
Value range: [0, 100, 1]
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
return self.data["target_position"]

@property
def adjust_value(self) -> int:
""" Adjust value
Value range: [-100, 100, 1]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Value range: [-100, 100, 1]

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does adjust value (and other properties, also) mean? The docstrings should be descriptive where possible :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is for the starting position limit. But right now I can't check it, because the curtain is in the process of installation.

"""
return self.data["adjust_value"]

def __repr__(self) -> str:
s = (
"<CurtainStatus"
"status=%s,"
"polarity=%s,"
"position_limit=%s,"
"night_tip_light=%s,"
"run_time=%s,"
"current_position=%s,"
"target_position=%s,"
"adjust_value=%s>"
% (
self.status,
self.polarity,
self.position_limit,
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 curtain which uses MIoT protocol."""
in7egral marked this conversation as resolved.
Show resolved Hide resolved

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.manual_enabled}\n"
"Polarity: {result.polarity}\n"
"Position limit: {result.position_limit}\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.
Values: Pause, Open, Close, Auto
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
try:
self.set_property("motor_control", motor_control.value)
except DeviceError as error:
raise
in7egral marked this conversation as resolved.
Show resolved Hide resolved

@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.
Value range: [0, 100, 1]
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
self.set_property("target_position", target_position)
in7egral marked this conversation as resolved.
Show resolved Hide resolved

@command(
click.argument("manual_enabled", type=EnumType(ManualEnabled)),
default_output=format_output("Set manual control {manual_enabled}"),
)
def set_manual_enabled(self, manual_enabled: ManualEnabled):
"""Set manual control of curtain.
Values: Enable, Disable
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
try:
self.set_property("manual_enabled", manual_enabled.value)
except DeviceError as error:
raise
in7egral marked this conversation as resolved.
Show resolved Hide resolved

@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.
Values: Positive, Reverse
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
try:
self.set_property("polarity", polarity.value)
except DeviceError as error:
raise

@command(
click.argument("pos_limit", type=EnumType(PosLimit)),
default_output=format_output("Set position limit to {pos_limit}"),
)
def set_position_limit(self, pos_limit: PosLimit):
"""Set position limit parameter.
Values: Unlimit, Limit
"""
try:
self.set_property("position_limit", pos_limit.value)
except DeviceError as error:
raise

@command(
click.argument("night_tip_light", type=EnumType(NightTipLight)),
default_output=format_output("Setting night tip light {night_tip_light"),
)
def set_night_tip_light(self, en_night_tip_light: NightTipLight):
"""Set night tip light.
Values: Enable, Disable
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
try:
self.set_property("night_tip_light", en_night_tip_light.value)
except DeviceError as error:
raise

""" motor_controller """
in7egral marked this conversation as resolved.
Show resolved Hide resolved
@command(
click.argument("adjust", type=int),
default_output=format_output("Set adjust value to {adjust}"),
)
def set_adjust_value(self, var: int):
"""Set adjust value.
Value range: [-100, 100, 1]
in7egral marked this conversation as resolved.
Show resolved Hide resolved
"""
self.set_property("adjust_value", var)