Skip to content

Commit

Permalink
Initial support for Vacuum 1C STYTJ01ZHM (dreame.vacuum.mc1808)
Browse files Browse the repository at this point in the history
  • Loading branch information
legacycode committed Feb 20, 2021
1 parent cb15f5d commit ad58bee
Show file tree
Hide file tree
Showing 3 changed files with 472 additions and 0 deletions.
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
376 changes: 376 additions & 0 deletions miio/dreamevacuum_miot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,376 @@
"""Vacuum 1C STYTJ01ZHM (dreame.vacuum.mc1808)
Commands:
home Return to home.
identify Locate the device (i am here).
info Get miIO protocol information from the device.
raw_command Send a raw command to the device.
reset_filter_life Reset filter life.
reset_mainbrush_life Reset main brush life.
reset_sidebrush_life Reset side brush life.
start Start cleaning.
status State of the vacuum.
stop Stop cleaning.
"""

import logging
from enum import Enum

from miio.miot_device import MiotDevice

from .click_common import command, format_output

_LOGGER = logging.getLogger(__name__)

_MAPPING = {
# siid 1: (Device Information): 4 props, 0 actions
# piid: 4 (Current Firmware Version): (string, unit: None) (acc: ['read'], value-list: [], value-range: None)
# piid: 1 (Device Manufacturer): (string, unit: None) (acc: ['read'], value-list: [], value-range: None)
# piid: 2 (Device Model): (string, unit: None) (acc: ['read'], value-list: [], value-range: None)
# piid: 3 (Device Serial Number): (string, unit: None) (acc: ['read'], value-list: [], value-range: None)
# siid 2: (Battery): 2 props, 1 actions
# piid: 1 (Battery Level): (uint8, unit: percentage) (acc: ['read', 'notify'], value-list: [], value-range: [0, 100, 1])
"battery_level": {"siid": 2, "piid": 1},
# piid: 2 (Charging State): (uint8, unit: None) (acc: ['read', 'notify'], value-list: [{'value': 1, 'description': 'Charging'}, {'value': 2, 'description': 'Not Charging'}, {'value': 4, 'description': 'Charging'}, {'value': 5, 'description': 'Go Charging'}], value-range: None)
"charging_state": {"siid": 2, "piid": 2},
# siid 3: (Robot Cleaner): 2 props, 2 actions
# piid: 1 (Device Fault): (uint8, unit: None) (acc: ['read', 'notify'], value-list: [{'value': 0, 'description': 'No faults'}], value-range: None)
"device_fault": {"siid": 3, "piid": 1},
# piid: 2 (Status): (int8, unit: None) (acc: ['read', 'notify'], value-list: [{'value': 1, 'description': 'Sweeping'}, {'value': 2, 'description': 'Idle'}, {'value': 3, 'description': 'Paused'}, {'value': 4, 'description': 'Error'}, {'value': 5, 'description': 'Go Charging'}, {'value': 6, 'description': 'Charging'}], value-range: None)
"device_status": {"siid": 3, "piid": 2},
# siid 17: (Identify): 0 props, 1 actions
# siid 26: (Main Cleaning Brush): 2 props, 1 actions
# piid: 1 (Brush Left Time): (uint16, unit: hour) (acc: ['read', 'notify'], value-list: [], value-range: [0, 300, 1])
"brush_left_time": {"siid": 26, "piid": 1},
# piid: 2 (Brush Life Level): (uint8, unit: percentage) (acc: ['read', 'notify'], value-list: [], value-range: [0, 100, 1])
"brush_life_level": {"siid": 26, "piid": 2},
# siid 27: (Filter): 2 props, 1 actions
# piid: 1 (Filter Life Level): (uint8, unit: percentage) (acc: ['read', 'notify'], value-list: [], value-range: [0, 100, 1])
"filter_life_level": {"siid": 27, "piid": 1},
# piid: 2 (Filter Left Time): (uint16, unit: hour) (acc: ['read', 'notify'], value-list: [], value-range: [0, 300, 1])
"filter_left_time": {"siid": 27, "piid": 2},
# siid 28: (Side Cleaning Brush): 2 props, 1 actions
# piid: 1 (Brush Left Time): (uint16, unit: hour) (acc: ['read', 'notify'], value-list: [], value-range: [0, 300, 1])
"brush_left_time2": {"siid": 28, "piid": 1},
# piid: 2 (Brush Life Level): (uint8, unit: percentage) (acc: ['read', 'notify'], value-list: [], value-range: [0, 100, 1])
"brush_life_level2": {"siid": 28, "piid": 2},
# siid 18: (clean): 16 props, 2 actions
# piid: 1 (Operating Mode): (int32, unit: none) (acc: ['read', 'notify'], value-list: [], value-range: [0, 17, 1])
"operating_mode": {"siid": 18, "piid": 1},
# piid: 4 (area): (string, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
# piid: 6 (Cleaning Mode): (int32, unit: none) (acc: ['read', 'write', 'notify'], value-list: [{'value': 0, 'description': 'quiet'}, {'value': 1, 'description': 'default'}, {'value': 2, 'description': 'medium'}, {'value': 3, 'description': 'strong'}], value-range: None)
"cleaning_mode": {"siid": 18, "piid": 6},
# piid: 8 (delete-timer): (int32, unit: None) (acc: ['write'], value-list: [], value-range: [0, 100, 1])
"delete_timer": {"siid": 18, "piid": 8},
# piid: 13 (): (uint32, unit: minutes) (acc: ['read', 'notify'], value-list: [], value-range: [0, 4294967295, 1])
#: int = field(metadata={'siid': 18, 'piid': 13, 'access': ['read', 'notify']})
# piid: 14 (): (uint32, unit: None) (acc: ['read', 'notify'], value-list: [], value-range: [0, 4294967295, 1])
#: int = field(metadata={'siid': 18, 'piid': 14, 'access': ['read', 'notify']})
# piid: 15 (): (uint32, unit: None) (acc: ['read', 'notify'], value-list: [], value-range: [0, 4294967295, 1])
#: int = field(metadata={'siid': 18, 'piid': 15, 'access': ['read', 'notify']})
# piid: 16 (): (uint32, unit: None) (acc: ['read', 'notify'], value-list: [], value-range: [0, 4294967295, 1])
#: int = field(metadata={'siid': 18, 'piid': 16, 'access': ['read', 'notify']})
# piid: 17 (): (uint16, unit: None) (acc: ['read', 'notify'], value-list: [], value-range: [0, 100, 1])
#: int = field(metadata={'siid': 18, 'piid': 17, 'access': ['read', 'notify']})
# piid: 18 (): (uint8, unit: None) (acc: ['read', 'notify'], value-list: [{'value': 0, 'description': ''}, {'value': 1, 'description': ''}], value-range: None)
#: int = field(metadata={'siid': 18, 'piid': 18, 'access': ['read', 'notify']})
# siid 19: (consumable): 3 props, 0 actions
# piid: 1 (life-sieve): (string, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
"life_sieve": {"siid": 19, "piid": 1},
# piid: 2 (life-brush-side): (string, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
"life_brush_side": {"siid": 19, "piid": 2},
# piid: 3 (life-brush-main): (string, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
"life_brush_main": {"siid": 19, "piid": 3},
# siid 20: (annoy): 3 props, 0 actions
# piid: 1 (enable): (bool, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
"timer_enable": {"siid": 20, "piid": 1},
# piid: 2 (start-time): (string, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
"start_time": {"siid": 20, "piid": 2},
# piid: 3 (stop-time): (string, unit: None) (acc: ['read', 'write'], value-list: [], value-range: None)
"stop_time": {"siid": 20, "piid": 3},
# siid 21: (remote): 2 props, 3 actions
# piid: 1 (deg): (string, unit: None) (acc: ['write'], value-list: [], value-range: None)
"deg": {"siid": 21, "piid": 1, "access": ["write"]},
# piid: 2 (speed): (string, unit: None) (acc: ['write'], value-list: [], value-range: None)
"speed": {"siid": 21, "piid": 2, "access": ["write"]},
# siid 22: (warn): 1 props, 0 actions
# siid 23: (map): 3 props, 1 actions
# piid: 1 (map-view): (string, unit: None) (acc: ['read', 'notify'], value-list: [], value-range: None)
"map_view": {"siid": 23, "piid": 1},
# piid: 2 (frame-info): (string, unit: None) (acc: ['write'], value-list: [], value-range: None)
"frame_info": {"siid": 23, "piid": 2},
# siid 24: (audio): 2 props, 3 actions
# piid: 1 (volume): (int32, unit: None) (acc: ['read', 'write', 'notify'], value-list: [], value-range: [0, 100, 1])
"volume": {"siid": 24, "piid": 1},
# piid: 3 (voice package): (string, unit: none) (acc: ['read', 'write'], value-list: [], value-range: None)
"voice_package": {"siid": 24, "piid": 3}
# siid 25: (): 1 props, 0 actions
# piid: 1 (): (string, unit: None) (acc: ['read', 'notify'], value-list: [], value-range: None)
# : str = field(metadata={'siid': 25, 'piid': 1, 'access': ['read', 'notify']})
}


class ChargingState(Enum):
unknown = -1
charging = 1
not_charging = 2
charging2 = 4
go_charging = 5


class CleaningMode(Enum):
unknown = -1
quiet = 0
default = 1
medium = 2
strong = 3


# TODO: Replace with correct operating modes
class OperatingMode(Enum):
unknown = -1
fixme0 = 0
fixme1 = 1
fixme14 = 14
fixme17 = 17


class FaultStatus(Enum):
unknown = -1
no_faults = 0


class DeviceStatus(Enum):
unknown = -1
sweeping = 1
idle = 2
paused = 3
error = 4
go_charging = 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)

# siid 3: (Robot Cleaner): 2 props, 2 actions
# aiid 1 Start Sweep: in: [] -> out: []
@command()
def start(self) -> None:
"""Start cleaning."""
return self.send_action(3, 1)

# aiid 2 Stop Sweeping: in: [] -> out: []
@command()
def stop(self) -> None:
"""Stop cleaning."""
return self.send_action(3, 2)

# siid 2: (Battery): 2 props, 1 actions
# aiid 1 Start Charge: in: [] -> out: []
@command()
def home(self) -> None:
"""Return to home."""
return self.send_action(2, 1)

# siid 17: (Identify): 0 props, 1 actions
# aiid 1 Identify: in: [] -> out: []
@command()
def identify(self) -> None:
"""Locate the device (i am here)."""
return self.send_action(17, 1)

# siid 26: (Main Cleaning Brush): 2 props, 1 actions
# aiid 1 Reset Brush Life: in: [] -> out: []
@command()
def reset_mainbrush_life(self) -> None:
"""Reset main brush life."""
return self.send_action(26, 1)

# siid 27: (Filter): 2 props, 1 actions
# aiid 1 Reset Filter Life: in: [] -> out: []
@command()
def reset_filter_life(self) -> None:
"""Reset filter life."""
return self.send_action(27, 1)

# siid 28: (Side Cleaning Brush): 2 props, 1 actions
# aiid 1 Reset Brush Life: in: [] -> out: []
@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."""

# 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
)
Loading

0 comments on commit ad58bee

Please sign in to comment.