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 Vacuum 1C STYTJ01ZHM (dreame.vacuum.mc1808) #952

Merged
merged 1 commit into from
Feb 24, 2021
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 @@ -96,6 +96,7 @@ Supported devices
- Xiaomi Aqara Gateway (basic implementation, alarm, lights)
- Xiaomi Mijia 360 1080p
- Xiaomi Mijia STYJ02YM (Viomi)
- Xiaomi Mijia 1C STYTJ01ZHM (Dreame)
- Xiaomi Mi Smart WiFi Socket
- Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port)
- Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports)
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from miio.cooker import Cooker
from miio.curtain_youpin import CurtainMiot
from miio.device import Device
from miio.dreamevacuum_miot import DreameVacuumMiot
from miio.exceptions import DeviceError, DeviceException
from miio.fan import Fan, FanP5, FanSA1, FanV2, FanZA1, FanZA4
from miio.fan_leshow import FanLeshow
Expand Down
292 changes: 292 additions & 0 deletions miio/dreamevacuum_miot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
"""Vacuum 1C STYTJ01ZHM (dreame.vacuum.mc1808)"""

import logging
from enum import Enum

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

_LOGGER = logging.getLogger(__name__)

_MAPPING = {
"battery_level": {"siid": 2, "piid": 1},
"charging_state": {"siid": 2, "piid": 2},
"device_fault": {"siid": 3, "piid": 1},
"device_status": {"siid": 3, "piid": 2},
"brush_left_time": {"siid": 26, "piid": 1},
"brush_life_level": {"siid": 26, "piid": 2},
"filter_life_level": {"siid": 27, "piid": 1},
"filter_left_time": {"siid": 27, "piid": 2},
"brush_left_time2": {"siid": 28, "piid": 1},
"brush_life_level2": {"siid": 28, "piid": 2},
"operating_mode": {"siid": 18, "piid": 1},
"cleaning_mode": {"siid": 18, "piid": 6},
"delete_timer": {"siid": 18, "piid": 8},
"life_sieve": {"siid": 19, "piid": 1},
"life_brush_side": {"siid": 19, "piid": 2},
"life_brush_main": {"siid": 19, "piid": 3},
"timer_enable": {"siid": 20, "piid": 1},
"start_time": {"siid": 20, "piid": 2},
"stop_time": {"siid": 20, "piid": 3},
"deg": {"siid": 21, "piid": 1, "access": ["write"]},
"speed": {"siid": 21, "piid": 2, "access": ["write"]},
"map_view": {"siid": 23, "piid": 1},
"frame_info": {"siid": 23, "piid": 2},
"volume": {"siid": 24, "piid": 1},
"voice_package": {"siid": 24, "piid": 3},
}


class ChargingState(Enum):
Unknown = -1
Charging = 1
Discharging = 2
Charging2 = 4
GoCharging = 5


class CleaningMode(Enum):
Unknown = -1
Quiet = 0
Default = 1
Medium = 2
Strong = 3


class OperatingMode(Enum):
Unknown = -1
Cleaning = 2
GoCharging = 3
Paused = 14


class FaultStatus(Enum):
Unknown = -1
NoFaults = 0


class DeviceStatus(Enum):
Unknown = -1
Sweeping = 1
Idle = 2
Paused = 3
Error = 4
GoCharging = 5
Charging = 6


class DreameVacuumStatus:
def __init__(self, data):
self.data = data

@property
def battery_level(self) -> str:
return self.data["battery_level"]

@property
def brush_left_time(self) -> str:
return self.data["brush_left_time"]

@property
def brush_left_time2(self) -> str:
return self.data["brush_left_time2"]

@property
def brush_life_level2(self) -> str:
return self.data["brush_life_level2"]

@property
def brush_life_level(self) -> str:
return self.data["brush_life_level"]

@property
def filter_left_time(self) -> str:
return self.data["filter_left_time"]

@property
def filter_life_level(self) -> str:
return self.data["filter_life_level"]

@property
def device_fault(self) -> FaultStatus:
try:
return FaultStatus(self.data["device_fault"])
except ValueError:
_LOGGER.error("Unknown FaultStatus (%s)", self.data["device_fault"])
return FaultStatus.Unknown

@property
def charging_state(self) -> ChargingState:
try:
return ChargingState(self.data["charging_state"])
except ValueError:
_LOGGER.error("Unknown ChargingStats (%s)", self.data["charging_state"])
return ChargingState.Unknown

@property
def operating_mode(self) -> OperatingMode:
try:
return OperatingMode(self.data["operating_mode"])
except ValueError:
_LOGGER.error("Unknown OperatingMode (%s)", self.data["operating_mode"])
return OperatingMode.Unknown

@property
def cleaning_mode(self) -> CleaningMode:
try:
return CleaningMode(self.data["cleaning_mode"])
except ValueError:
_LOGGER.error("Unknown CleaningMode (%s)", self.data["cleaning_mode"])
return CleaningMode.Unknown

@property
def device_status(self) -> DeviceStatus:
try:
return DeviceStatus(self.data["device_status"])
except TypeError:
_LOGGER.error("Unknown DeviceStatus (%s)", self.data["device_status"])
return DeviceStatus.Unknown

@property
def life_sieve(self) -> str:
return self.data["life_sieve"]

@property
def life_brush_side(self) -> str:
return self.data["life_brush_side"]

@property
def life_brush_main(self) -> str:
return self.data["life_brush_main"]

@property
def timer_enable(self) -> str:
return self.data["timer_enable"]

@property
def start_time(self) -> str:
return self.data["start_time"]

@property
def stop_time(self) -> str:
return self.data["stop_time"]

@property
def map_view(self) -> str:
return self.data["map_view"]

@property
def volume(self) -> str:
return self.data["volume"]

@property
def voice_package(self) -> str:
return self.data["voice_package"]


class DreameVacuumMiot(MiotDevice):
"""Interface for Vacuum 1C STYTJ01ZHM (dreame.vacuum.mc1808)"""

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

@command(
default_output=format_output(
"\n",
"Battery level: {result.battery_level}\n"
"Brush life level: {result.brush_life_level}\n"
"Brush left time: {result.brush_left_time}\n"
"Charging state: {result.charging_state.name}\n"
"Cleaning mode: {result.cleaning_mode.name}\n"
"Device fault: {result.device_fault.name}\n"
"Device status: {result.device_status.name}\n"
"Filter left level: {result.filter_left_time}\n"
"Filter life level: {result.filter_life_level}\n"
"Life brush main: {result.life_brush_main}\n"
"Life brush side: {result.life_brush_side}\n"
"Life sieve: {result.life_sieve}\n"
"Map view: {result.map_view}\n"
"Operating mode: {result.operating_mode.name}\n"
"Side cleaning brush left time: {result.brush_left_time2}\n"
"Side cleaning brush life level: {result.brush_life_level2}\n"
"Timer enabled: {result.timer_enable}\n"
"Timer start time: {result.start_time}\n"
"Timer stop time: {result.stop_time}\n"
"Voice package: {result.voice_package}\n"
"Volume: {result.volume}\n",
)
)
def status(self) -> DreameVacuumStatus:
"""State of the vacuum."""

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

def send_action(self, siid, aiid, params=None):
"""Send action to device."""

# {"did":"<mydeviceID>","siid":18,"aiid":1,"in":[{"piid":1,"value":2}]
if params is None:
params = []
payload = {
"did": f"call-{siid}-{aiid}",
"siid": siid,
"aiid": aiid,
"in": params,
}
return self.send("action", payload)

@command()
def start(self) -> None:
"""Start cleaning."""
return self.send_action(3, 1)

@command()
def stop(self) -> None:
"""Stop cleaning."""
return self.send_action(3, 2)

@command()
def home(self) -> None:
"""Return to home."""
return self.send_action(2, 1)

@command()
def identify(self) -> None:
"""Locate the device (i am here)."""
return self.send_action(17, 1)

@command()
def reset_mainbrush_life(self) -> None:
"""Reset main brush life."""
return self.send_action(26, 1)

@command()
def reset_filter_life(self) -> None:
"""Reset filter life."""
return self.send_action(27, 1)

@command()
def reset_sidebrush_life(self) -> None:
"""Reset side brush life."""
return self.send_action(28, 1)

def get_properties_for_mapping(self) -> list:
"""Retrieve raw properties based on mapping.

Method was copied from the base class to change the value of max_properties to
10. This change is needed to avoid "Checksum error" messages from the device.
"""

# We send property key in "did" because it's sent back via response and we can identify the property.
properties = [{"did": k, **v} for k, v in self.mapping.items()]

return self.get_properties(
properties, property_getter="get_properties", max_properties=10
)
2 changes: 1 addition & 1 deletion miio/heater.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def __init__(
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover)

if model in SUPPORTED_MODELS.keys():
if model in SUPPORTED_MODELS:
self.model = model
else:
self.model = MODEL_HEATER_ZA1
Expand Down
Loading