Skip to content

Commit

Permalink
JSON usage
Browse files Browse the repository at this point in the history
  • Loading branch information
basveeling committed Oct 22, 2021
1 parent 4055acb commit 9ab1c8e
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 25 deletions.
110 changes: 85 additions & 25 deletions miio/ihcooker.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enum
import json
import logging
import random
import warnings
Expand All @@ -24,6 +25,8 @@

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,
Expand All @@ -40,7 +43,7 @@
DEFAULT_THRESHOLD_CELCIUS = 249
DEFAULT_TEMP_TARGET_CELCIUS = 229
DEFAULT_FIRE_LEVEL = 45
DEFAULT_PHASE_MINUTES = 0
DEFAULT_PHASE_MINUTES = 50


def crc16(data: bytes, offset=0, length=None):
Expand All @@ -51,10 +54,10 @@ def crc16(data: bytes, offset=0, length=None):
if length is None:
length = len(data)
if (
data is None
or offset < 0
or offset > len(data) - 1
and offset + length > len(data)
data is None
or offset < 0
or offset > len(data) - 1
and offset + length > len(data)
):
return 0
crc = 0x0000
Expand All @@ -77,10 +80,11 @@ class StageMode(enum.IntEnum):

FireMode = 0
TemperatureMode = 2
Unknown1 = 4
Unknown4 = 4
TempAutoSmallPot = 8 # TODO: verify this is the right behaviour.
Unknown10 = 10
TempAutoBigPot = 24 # TODO: verify this is the right behaviour.
Unknown2 = 16
Unknown16 = 16


class OperationMode(enum.Enum):
Expand Down Expand Up @@ -190,7 +194,7 @@ def profile_base(is_v1, recipe_name_encoding="GBK"):
c.Const(3, c.Int8un),
"device_version" / c.Default(c.Enum(c.Int8ub, **DEVICE_ID), 1 if is_v1 else 2),
"menu_location"
/ c.Default(c.ExprValidator(c.Int8ub, lambda o, _: 0 < o and o < 10), 9),
/ c.Default(c.ExprValidator(c.Int8ub, lambda o, _: 0 <= o < 10), 9),
"recipe_name"
/ c.Default(
c.ExprAdapter(
Expand Down Expand Up @@ -551,6 +555,8 @@ class IHCooker(Device):
Custom recipes can be build with the profile_v1/v2 structure.
"""

_supported_models = SUPPORTED_MODELS

@command(
default_output=format_output(
"",
Expand Down Expand Up @@ -610,12 +616,14 @@ def status(self) -> IHCookerStatus:

@command(
click.argument("profile", type=str),
click.argument("skip_confirmation", type=bool),
click.argument("skip_confirmation", type=bool, default=False),
default_output=format_output("Cooking profile requested."),
)
def start(self, profile: Union[str, c.Container, dict], skip_confirmation=False):
"""Start cooking a profile.
:arg
Please do not use skip_confirmation=True, as this is potentially unsafe.
"""

Expand All @@ -639,12 +647,12 @@ def start(self, profile: Union[str, c.Container, dict], skip_confirmation=False)
default_output=format_output("Cooking with temperature requested."),
)
def start_temp(
self,
temperature,
minutes=60,
power=DEFAULT_FIRE_LEVEL,
skip_confirmation=False,
menu_location=9,
self,
temperature,
minutes=60,
power=DEFAULT_FIRE_LEVEL,
skip_confirmation=False,
menu_location=9,
):
"""Start cooking at a fixed temperature and duration.
Expand All @@ -670,7 +678,7 @@ def start_temp(
profile = self._prepare_profile(profile)

if menu_location != 9:
self.set_menu(profile, menu_location, False)
self.set_menu(profile, menu_location, True)
else:
self.start(profile, skip_confirmation)

Expand Down Expand Up @@ -720,22 +728,69 @@ def factory_reset(self):

self.send("set_factory_reset", [self._device_prefix])

@command(default_output=format_output("WiFi led setting changed."))
@command(
click.argument("profile", type=str),
default_output=format_output(""),
)
def profile_to_json(self, profile: Union[str, c.Container, dict]):
"""Convert profile to json."""
profile = self._prepare_profile(profile)

res = dict(profile)
res["menu_settings"] = dict(res["menu_settings"])
del res["menu_settings"]["_io"]
del res["_io"]
del res["crc"]
res["stages"] = [
{k: v for k, v in s.items() if k != "_io"} for s in res["stages"]
]

return json.dumps(res)

@command(
click.argument("json_str", type=str),
default_output=format_output(""),
)
def json_to_profile(self, json_str: str):
"""Convert json to profile."""

profile = self._profile_obj.build(self._prepare_profile(json.loads(json_str)))

return str(profile.hex())

@command(
click.argument("value", type=bool),
default_output=format_output("WiFi led setting changed."),
)
def set_wifi_led(self, value: bool):
"""Keep wifi-led on when idle."""
return self.send(
"set_wifi_state", [self._device_prefix + "01" if value else "00"]
)

@command(
click.argument("power", type=int),
default_output=format_output("Fire power set."),
)
def set_power(self, power: int):
"""Set fire power."""
if not 0 <= power < 100:
raise ValueError("Power should be in range [0,99]")
return self.send(
"set_fire", [self._device_prefix + "0005"]
) # + f'{power:02x}'])

@command(
click.argument("profile", type=str),
default_output=format_output("Setting menu to {profile}"),
click.argument("location", type=int),
click.argument("confirm_start", type=bool),
default_output=format_output("Setting menu."),
)
def set_menu(
self,
profile: Union[str, c.Container, dict],
location: int,
skip_confirmation=False,
self,
profile: Union[str, c.Container, dict],
location: int,
confirm_start=False,
):
"""Updates one of the menu options with the profile.
Expand All @@ -744,10 +799,11 @@ def set_menu(
- skip_confirmation, if True, request confirmation to start recipe as well.
"""
profile = self._prepare_profile(profile)
print(profile)
if location >= 9 or location < 1:
raise IHCookerException("location %d must be in [1,8]." % location)
profile.menu_settings.save_recipe = True
profile.confirm_start = not skip_confirmation
profile.confirm_start = confirm_start
profile.menu_location = location

self.send("set_menu1", [self._profile_obj.build(profile).hex()])
Expand All @@ -763,8 +819,12 @@ def _profile_obj(self) -> c.Struct:

def _prepare_profile(self, profile: Union[str, c.Container, dict]) -> c.Container:
if isinstance(profile, str):
profile = self._profile_obj.parse(bytes.fromhex(profile))
elif isinstance(profile, dict):
if profile.strip().startswith("{"):
# Assuming JSON string.
profile = json.loads(profile)
else:
profile = self._profile_obj.parse(bytes.fromhex(profile))
if isinstance(profile, dict):
for k in profile.keys():
if k not in profile_keys:
raise ValueError("Invalid key %s in profile dict." % k)
Expand Down
31 changes: 31 additions & 0 deletions miio/tests/test_ihcooker.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,37 @@ def test_set_menu(self):
def test_start_temp(self):
self.device.start_temp(temperature=30, minutes=30)

def test_start_json(self):
json_str = """{
"menu_location": 5,
"recipe_name": "Rice Cooking",
"recipe_id": 42,
"duration_minutes": 300,
"stages": [
{
"mode": "Unknown10",
"temp_threshold": 60,
"temp_target": 90,
"power": 99
},
{
"mode": "Unknown10",
"temp_threshold": 91,
"temp_target": 102,
"power": 10
},
{
"mode": "TempAutoSmallPot",
"minutes": 300,
"temp_threshold": 150,
"temp_target": 60,
"power": 10
}
]
}
"""
self.device.start(json_str)

def test_construct(self):
recipe = (
"030405546573740a52656369706500000000000000000000000000000000000000000003ea0000000"
Expand Down

0 comments on commit 9ab1c8e

Please sign in to comment.