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

Support for the Xiaomi Induction Cooker #832

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions docs/api/miio.ihcooker.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
miio.ihcooker module
====================

.. automodule:: miio.ihcooker
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/api/miio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Submodules
miio.heater
miio.heater_miot
miio.huizuo
miio.ihcooker
miio.miioprotocol
miio.miot_device
miio.philips_bulb
Expand Down
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from miio.heater import Heater
from miio.heater_miot import HeaterMiot
from miio.huizuo import Huizuo, HuizuoLampFan, HuizuoLampHeater, HuizuoLampScene
from miio.integrations.ihcooker.ihcooker import IHCooker
rytilahti marked this conversation as resolved.
Show resolved Hide resolved
from miio.integrations.yeelight import Yeelight
from miio.miot_device import MiotDevice
from miio.philips_bulb import PhilipsBulb, PhilipsWhiteBulb
Expand Down
18 changes: 17 additions & 1 deletion miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@

import zeroconf

from miio import IHCooker
from miio.integrations.ihcooker import (
MODEL_EG1,
MODEL_EXP1,
MODEL_FW,
MODEL_HK1,
MODEL_KOREA1,
MODEL_TW1,
MODEL_V1,
)
basveeling marked this conversation as resolved.
Show resolved Hide resolved
from miio.integrations.yeelight import Yeelight

from . import (
Expand Down Expand Up @@ -101,7 +111,6 @@

_LOGGER = logging.getLogger(__name__)


DEVICE_MAP: Dict[str, Union[Type[Device], partial]] = {
"rockrobo-vacuum-v1": Vacuum,
"roborock-vacuum-s5": Vacuum,
Expand Down Expand Up @@ -174,6 +183,13 @@
"chunmi-cooker-normal3": Cooker,
"chunmi-cooker-normal4": Cooker,
"chunmi-cooker-normal5": Cooker,
MODEL_EXP1: IHCooker,
MODEL_FW: IHCooker,
MODEL_TW1: IHCooker,
MODEL_KOREA1: IHCooker,
MODEL_HK1: IHCooker,
MODEL_V1: IHCooker,
MODEL_EG1: IHCooker,
Comment on lines +186 to +192
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think these are correct for mdns? Either remove these completely for now, or check what format is being used in mdns on your device and just add them here as regular strings.

This information should be moved inside the integration itself at some point, but it'll require some more work. Having these here as strings makes it easy to copy them over if/when that time comes.

Copy link
Author

Choose a reason for hiding this comment

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

Removed.

Copy link
Owner

Choose a reason for hiding this comment

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

They seem to be still there in the diff 👀

"lumi-acpartner-v1": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V1),
"lumi-acpartner-v2": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V2),
"lumi-acpartner-v3": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V3),
Expand Down
62 changes: 62 additions & 0 deletions miio/integrations/ihcooker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import enum
import logging

from miio.exceptions import DeviceException

_LOGGER = logging.getLogger(__name__)

MODEL_EG1 = "chunmi.ihcooker.eg1"
MODEL_EXP1 = "chunmi.ihcooker.exp1"
MODEL_FW = "chunmi.ihcooker.chefnic"
MODEL_HK1 = "chunmi.ihcooker.hk1"
MODEL_KOREA1 = "chunmi.ihcooker.korea1"
MODEL_TW1 = "chunmi.ihcooker.tw1"
MODEL_V1 = "chunmi.ihcooker.v1"

MODEL_VERSION1 = [MODEL_V1, MODEL_FW, MODEL_HK1, MODEL_TW1]
MODEL_VERSION2 = [MODEL_EG1, MODEL_EXP1, MODEL_KOREA1]
SUPPORTED_MODELS = MODEL_VERSION1 + MODEL_VERSION2

DEVICE_ID = {
MODEL_EG1: 4,
MODEL_EXP1: 4,
MODEL_FW: 7,
MODEL_HK1: 2,
MODEL_KOREA1: 5,
MODEL_TW1: 3,
MODEL_V1: 1,
}


class IHCookerException(DeviceException):
pass


class OperationMode(enum.Enum):
"""Global mode the induction cooker is currently in."""

Error = "error"
Finish = "finish"
Offline = "offline"
Pause = "pause"
TimerPaused = "pause_time"
Precook = "precook"
Running = "running"
SetClock = "set01"
SetStartTime = "set02"
SetCookingTime = "set03"
Shutdown = "shutdown"
Timing = "timing"
Waiting = "waiting"


class StageMode(enum.IntEnum):
"""Mode for current stage of recipe."""

FireMode = 0
TemperatureMode = 2
Unknown4 = 4
TempAutoSmallPot = 8 # TODO: verify this is the right behaviour.
Unknown10 = 10
TempAutoBigPot = 24 # TODO: verify this is the right behaviour.
Unknown16 = 16
84 changes: 84 additions & 0 deletions miio/integrations/ihcooker/custom_construct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import construct as c


class ArrayDefault(c.Array):
r"""
Homogenous array of elements, similar to C# generic T[].

Parses into a ListContainer (a list). Parsing and building processes an exact amount of elements. If given list has less than count elements, the array is padded with the default element. More elements raises RangeError. Size is defined as count multiplied by subcon size, but only if subcon is fixed size.

Operator [] can be used to make Array instances (recommended syntax).

:param count: integer or context lambda, strict amount of elements
:param subcon: Construct instance, subcon to process individual elements
:param default: default element to pad array with.
:param discard: optional, bool, if set then parsing returns empty list

:raises StreamError: requested reading negative amount, could not read enough bytes, requested writing different amount than actual data, or could not write all bytes
:raises RangeError: specified count is not valid
:raises RangeError: given object has different length than specified count

Can propagate any exception from the lambdas, possibly non-ConstructError.

Example::

>>> d = ArrayDefault(5, Byte, 0) or Byte[5]
>>> d.build(range(3))
b'\x00\x01\x02\x00\x00'
>>> d.parse(_)
[0, 1, 2, 0, 0]
"""

def __init__(self, count, subcon, default, discard=False):
super(ArrayDefault, self).__init__(count, subcon, discard)
self.default = default

def _build(self, obj, stream, context, path):
count = self.count
if callable(count):
count = count(context)
if not 0 <= count:
raise c.RangeError("invalid count %s" % (count,), path=path)
if len(obj) > count:
raise c.RangeError(
"expected %d elements, found %d" % (count, len(obj)), path=path
)
retlist = c.ListContainer()

for i, e in enumerate(obj):
context._index = i
buildret = self.subcon._build(e, stream, context, path)
retlist.append(buildret)
for i in range(len(obj), count):
context._index = i
buildret = self.subcon._build(self.default, stream, context, path)
retlist.append(buildret)
return retlist


class RebuildStream(c.Rebuild):
r"""
Field where building does not require a value, because the value gets recomputed when needed. Comes handy when building a Struct from a dict with missing keys. Useful for length and count fields when :class:`~construct.core.Prefixed` and :class:`~construct.core.PrefixedArray` cannot be used.

Parsing defers to subcon. Building is defered to subcon, but it builds from a value provided by the stream until now. Size is the same as subcon, unless it raises SizeofError.

Difference between Rebuild and RebuildStream, is that RebuildStream provides the current datastream to func.

:param subcon: Construct instance
:param func: lambda that works with streamed bytes up to this point.

:raises StreamError: requested reading negative amount, could not read enough bytes, requested writing different amount than actual data, or could not write all bytes

Can propagate any exception from the lambda, possibly non-ConstructError.
"""

def __init__(self, subcon, func):
super(RebuildStream, self).__init__(subcon, func)

def _build(self, obj, stream, context, path):
fallback = c.stream_tell(stream, path)
c.stream_seek(stream, 0, 0, path)
data = stream.read(fallback)
obj = self.func(data) if callable(self.func) else self.func
ret = self.subcon._build(obj, stream, context, path)
return ret
9 changes: 9 additions & 0 deletions miio/integrations/ihcooker/data/ihcooker_profiles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"MODEL_V2": [
{
"title": "Pan-fried bacon",
"description": "Pan-fried bacon",
"profile": "030200bce5c5e0b8f90000000000000000000000000000000000000000000000000000000ce1000f040000000000010000800fd2c8501414008000d2b9011414008000d2b95a1414008000d2b95a1414008000d2b95a1414008000d2b95a1414008000d2b9371414008000d2b9001414008000d2b9001414008000d2b9001414008000d2b9001414008000d2b9001414008000d2b9001414008000d2b900141400ffffd2b9001414000000000000000000b2f8"
}
]
}
28 changes: 28 additions & 0 deletions miio/integrations/ihcooker/data/ihcooker_rice_recipe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"recipe_name": "Rice Cooking",
"recipe_id": 42,
"duration_minutes": 300,
"stages": [
{
"mode": "Unknown10",
"temp_threshold": 60,
"temp_target": 100,
"power": 99
},
{
"mode": "Unknown10",
"temp_threshold": 95,
"temp_target": 102,
"power": 10
},
{
"mode": "TempAutoSmallPot",
"minutes": 300,
"temp_threshold": 150,
"temp_target": 60,
"power": 10,
"fire_off": 20,
"fire_on": 20
}
]
}
Loading