xiaomi_cooker icon indicating copy to clipboard operation
xiaomi_cooker copied to clipboard

Editing Cooker Profiles

Open strup8 opened this issue 4 years ago • 6 comments

Hello, do you know the format of profile property in cooker_profile.json? Is it possible to decode it and make my own custom cooking profile?

Thank you.

strup8 avatar Jan 17 '20 06:01 strup8

__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  var _interopRequireDefault = _$$_REQUIRE(_dependencyMap[0]);

  Object.defineProperty(exports, "__esModule", {
    value: true
  });
  exports.default = undefined;

  var _classCallCheck2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[1]));

  var _createClass2 = _interopRequireDefault(_$$_REQUIRE(_dependencyMap[2]));

  var _reactNative = _$$_REQUIRE(_dependencyMap[3]);

  var CookProfile = function () {
    function CookProfile() {
      (0, _classCallCheck2.default)(this, CookProfile);

      this._mergeByte = function (b1, b2) {
        var i = b1;
        i = i << 8;
        return i | b2 & 0xFF;
      };
    }

    (0, _createClass2.default)(CookProfile, [{
      key: "formData",
      value: function formData(realData) {
        if (realData === undefined || realData === null || realData === '') {
          return;
        }

        if (realData.length < 25) return null;
        var rdata = new Array();
        var j = 0;

        for (var i = 0; i < realData.length; i += 2) {
          var sh = realData.substring(i, i + 2);
          rdata[j++] = Number.parseInt(sh, 16) & 0xFF;
        }

        var cookProfile = new CookProfile();
        cookProfile.data = rdata;
        return cookProfile;
      }
    }, {
      key: "toHexData",
      value: function toHexData() {
        var crc16 = this.calcrc(this.data, this.data.length - 2);
        this.data[this.data.length - 2] = crc16 >> 8 & 0xFF;
        this.data[this.data.length - 1] = crc16 & 0xFF;
        var sb = '';

        for (var i = 0; i < this.data.length; i++) {
          var dh = (this.data[i] & 0xFF).toString(16);

          if (dh.length < 2) {
            dh = '0' + dh;
          }

          sb = sb.concat(dh);
        }

        return sb.toString();
      }
    }, {
      key: "isCanDisplayLocation",
      value: function isCanDisplayLocation() {
        return this.getId() == 1 || this.getId() == 2;
      }
    }, {
      key: "isCanChooseRice",
      value: function isCanChooseRice() {
        return this.getId() <= 2;
      }
    }, {
      key: "isCanConfigTaste",
      value: function isCanConfigTaste() {
        return this.getId() == 1;
      }
    }, {
      key: "isCanSetTime",
      value: function isCanSetTime() {
        return this.data[5] != this.data[7] || this.data[6] != this.data[8];
      }
    }, {
      key: "calcrc",
      value: function calcrc(tData, count) {
        var crc = 0;
        var j = 0;

        while (--count >= 0) {
          crc = crc ^ tData[j++].toString(10) << 8;

          for (var i = 0; i < 8; ++i) {
            if ((crc & 0x8000) != 0) {
              crc = crc << 1 ^ 0x1021;
            } else {
              crc <<= 1;
            }
          }
        }

        return crc & 0xFFFF;
      }
    }, {
      key: "getType",
      value: function getType() {
        return this.data[0];
      }
    }, {
      key: "getIndex",
      value: function getIndex() {
        return this.data[2];
      }
    }, {
      key: "setIndex",
      value: function setIndex(index) {
        this.data[2] = index;
      }
    }, {
      key: "getId",
      value: function getId() {
        return this._mergeByte(this.data[0], this.data[1]);
      }
    }, {
      key: "setId",
      value: function setId(recipeId) {
        this.data[0] = recipeId >> 8 & 0xFF;
        this.data[1] = recipeId & 0xFF;
      }
    }, {
      key: "setFavorite",
      value: function setFavorite(isFavorite) {
        if (isFavorite) this.data[2] = this.data[2] | 0x80;else this.data[2] = this.data[2] & 0x7F;
      }
    }, {
      key: "isFavorite",
      value: function isFavorite() {
        return (this.data[2] & 0x80) != 0;
      }
    }, {
      key: "canFavorite",
      value: function canFavorite() {
        return this.getId() > 4;
      }
    }, {
      key: "isScheduleEnabled",
      value: function isScheduleEnabled() {
        return (this.data[2] & 0x40) != 0;
      }
    }, {
      key: "isCanAutoKeepWarm",
      value: function isCanAutoKeepWarm() {
        return (this.data[2] & 0x20) != 0;
      }
    }, {
      key: "getDuration",
      value: function getDuration() {
        return this.data[3] * 60 + this.data[4];
      }
    }, {
      key: "setDuration",
      value: function setDuration(duration) {
        this.data[3] = Math.floor(duration / 60);
        this.data[4] = duration % 60;
      }
    }, {
      key: "getDurationMax",
      value: function getDurationMax() {
        return this.data[5] * 60 + this.data[6];
      }
    }, {
      key: "getDurationMaxHour",
      value: function getDurationMaxHour() {
        return this.data[5];
      }
    }, {
      key: "getDurationMaxMinute",
      value: function getDurationMaxMinute() {
        return this.data[6];
      }
    }, {
      key: "getDurationMin",
      value: function getDurationMin() {
        return this.data[7] * 60 + this.data[8];
      }
    }, {
      key: "getDurationMinHour",
      value: function getDurationMinHour() {
        return this.data[7];
      }
    }, {
      key: "getDurationMinMinute",
      value: function getDurationMinMinute() {
        return this.data[8];
      }
    }, {
      key: "setScheduleEnabled",
      value: function setScheduleEnabled(enabled) {
        if (enabled) {
          this.data[9] |= 0x80;
        } else {
          this.data[9] &= 0x7F;
        }
      }
    }, {
      key: "setScheduleDuration",
      value: function setScheduleDuration(schedule) {
        var scheduleFlag = this.data[9] & 0x80;
        this.data[9] = schedule / 60 & 0xFF;
        this.data[9] |= scheduleFlag;
        this.data[10] = (schedule % 60 | this.data[10] & 0x80) & 0xFF;
      }
    }, {
      key: "getTasteId",
      value: function getTasteId() {
        return this.data[7];
      }
    }, {
      key: "setTasteId",
      value: function setTasteId(tasteId) {
        this.data[7] = tasteId & 0xFF;
      }
    }, {
      key: "setCanAutoKeepWarm",
      value: function setCanAutoKeepWarm(canAutoKeepWarm) {
        if (canAutoKeepWarm) this.data[10] = this.data[10] | 0x80;else this.data[10] = this.data[10] & 0x7F;
      }
    }, {
      key: "isAutoKeepWarmOpened",
      value: function isAutoKeepWarmOpened() {
        return (this.data[10] & 0x80) == 0x80;
      }
    }]);
    return CookProfile;
  }();

  exports.default = CookProfile;
},10322,[14305,14320,14323,10033]);

syssi avatar Oct 30 '20 12:10 syssi

@syssi Thanks a lot for you work on this integration. It looks like this data is from the mi home app, and it unfortunately covers only the first 10 or so bytes + 2 bytes crc out of 242 bytes. Any idea on what the rest does?

nekromant avatar May 24 '21 11:05 nekromant

Take a look at this PR: https://github.com/rytilahti/python-miio/pull/832

I assume it implements everything you are looking for.

syssi avatar May 24 '21 12:05 syssi

Thanks for the hint. Looks interesting. I wonder if the format of ihcooker profiles differs much from what we have here.

nekromant avatar May 24 '21 12:05 nekromant

To answer your first question: If I remember correctly most of the recipe is a time series of target temperatures. I don't know the resolution.

syssi avatar May 24 '21 12:05 syssi

Thanks for all the hints, I think I got the very basic profile editing (time and max time only) with a dumb python script. This pretty much covers most of typical uses, although it lacks more fine-grained tuning. I'm posting my script here.

import sys
import crc16

class CookerProfile():
    data = bytearray()

    def __init__(self, prof : str):
        self.load(prof)

    def load(self, profile):
        while len(profile):
            self.data = self.data + int(profile[:2],16).to_bytes(1,sys.byteorder)
            profile = profile[2:]

    def fixcrc(self):
        crc = crc16.crc16xmodem(bytes(self.data[0:-2]))
        self.data[-1] = (crc & 0xff)
        self.data[-2] = ((crc >> 8) & 0xff)

    def duration(self, duration_minutes = None):
        if duration_minutes != None:
            self.data[3] = int(duration_minutes / 60)
            self.data[4] = duration_minutes % 60
        d = self.data[3] * 60 + self.data[4]
        print(f"duration: {self.data[3]} hrs {self.data[4]} min ({d} min)")

    def duration_max(self, duration_minutes = None):
        if duration_minutes != None:
            self.data[5] = int(duration_minutes / 60)
            self.data[6] = duration_minutes % 60
        d = self.data[5] * 60 + self.data[6]
        print(f"max duration: {self.data[5]} hrs {self.data[6]} min ({d} min)")


    def dump(self):
        pr = ''.join('{:02x}'.format(x) for x in self.data)
        print(pr)


pdata = "0003E2011E040000280080000190551C0601001E00000000000001B8551C0601002300000000000001E0561C0600002E000000000000FFFF571C0600003000000000000000280A0082001E914E730E01001E82FF736E0610FF756E02690A1E75826E0269100F75826E0269100069005A0000000000000000CB"
cook = CookerProfile(pdata)
cook.duration()
cook.duration(10)
cook.duration_max()
cook.fixcrc()
cook.dump()

I wonder if it's possible to add profile selection + time adjustment to the integration itself.

nekromant avatar May 26 '21 09:05 nekromant