Anko Smart Kettle

Just thought I’d post some brief observations about the Anko Smart Kettle (LD-K3068, $65 from K-Mart in Australia) and my experiences replacing the ESP8266 (ESP8285) controller software with esphome.

  • I didn’t attempt an OTA exploit, I disassembled it (surprisingly no fancy screws involved), soldered short flyleads to the necessary pins and flashed it over serial.
  • The kettle has its own chip that controls the relay, the LEDs, the buttons and the beeper. Core kettle functionality doesn’t rely on the ESP8266 at all. The ESP8266 can do little more than read the temperature, observe the current modes and push some virtual buttons. So if you wanted to be able to do fun things with the LEDs or make the kettle less beepy, you’re out of luck.
  • The controller chip is a Tuya device which esphome can communicate with. However Esphome doesn’t currently have a Tuya profile that works with it, so I had to write a few hacky lambdas to push the buttons.
  • The temperature sensor is generally quite accurate (or at least within 1° of my Thermoworks ChefAlarm) although it has severe hysteresis i.e. it lags quite a bit. The controller seems to be programmed with this hysteresis in mind, as it does some very basic things to compensate for it. But it doesn’t compensate well when the water level is low.
  • For a short period before and after reaching the target, the controller stops giving you accurate thermal readings and instead offers you lies. It’s pretending that it hit the target temperature perfectly when in reality it tends to overshoot quite a bit. (If you ask for 80° the water will get as hot as 90° depending on the quantity of water in the kettle.)
  • You can tell when the kettle is not on the base because the reported temperature is zero. Otherwise it will always report the water temperature. It doesn’t seem to ever go to deep sleep.
  • I haven’t experimented with its dry boil protection, assuming it has any.

Here’s the current iteration of my YAML:

packages:
  core: !include _config/core.yaml
  
esphome:
  platform: ESP8266
  board: esp8285
  on_boot:
    then:
      - text_sensor.template.publish:
          id: text_state
          state: "Ready"

logger:
  level: DEBUG

uart:
  rx_pin: GPIO13
  tx_pin: GPIO15
  baud_rate: 9600

tuya:

text_sensor:
  - platform: template
    name: Kettle State
    id: text_state
  
switch:
  - platform: tuya
    id: the_relay
    switch_datapoint: 1
    
  - platform: tuya
    name: Kettle Keep Warm
    switch_datapoint: 13
    
  - platform: template
    name: Kettle Heat To 80
    lambda: |-
      return (id(kettle_target).state == 80 && (id(tuya_15).state == 1 || id(tuya_15).state == 3) );
    turn_on_action:
      - switch.turn_off: the_relay
      - lambda: |-
          tuya::TuyaDatapoint datapoint{};
          datapoint.id = 4;
          datapoint.type = tuya::TuyaDatapointType::ENUM;
          datapoint.value_enum = 0x0;
          tuya_tuya->set_datapoint_value(datapoint);
      - sensor.template.publish:
          id: kettle_target
          state: 80
      - switch.turn_on: the_relay
    turn_off_action:
      - switch.turn_off: the_relay
  
  
  - platform: template
    name: Kettle Heat To 85
    lambda: |-
      return (id(kettle_target).state == 85 && (id(tuya_15).state == 1 || id(tuya_15).state == 3) );
    turn_on_action:
      - switch.turn_off: the_relay
      - lambda: |-
          tuya::TuyaDatapoint datapoint{};
          datapoint.id = 4;
          datapoint.type = tuya::TuyaDatapointType::ENUM;
          datapoint.value_enum = 0x1;
          tuya_tuya->set_datapoint_value(datapoint);
      - sensor.template.publish:
          id: kettle_target
          state: 85
      - switch.turn_on: the_relay
    turn_off_action:
      - switch.turn_off: the_relay
      
    
  - platform: template
    name: Kettle Heat To 90
    lambda: |-
      return (id(kettle_target).state == 90 && (id(tuya_15).state == 1 || id(tuya_15).state == 3) );
    turn_on_action:
      - switch.turn_off: the_relay
      - lambda: |-
          tuya::TuyaDatapoint datapoint{};
          datapoint.id = 4;
          datapoint.type = tuya::TuyaDatapointType::ENUM;
          datapoint.value_enum = 0x2;
          tuya_tuya->set_datapoint_value(datapoint);
      - sensor.template.publish:
          id: kettle_target
          state: 90
      - switch.turn_on: the_relay
    turn_off_action:
      - switch.turn_off: the_relay
  
  
  - platform: template
    name: Kettle Heat To 95
    lambda: |-
      return (id(kettle_target).state == 95 && (id(tuya_15).state == 1 || id(tuya_15).state == 3) );
    turn_on_action:
      - switch.turn_off: the_relay
      - lambda: |-
          tuya::TuyaDatapoint datapoint{};
          datapoint.id = 4;
          datapoint.type = tuya::TuyaDatapointType::ENUM;
          datapoint.value_enum = 0x3;
          tuya_tuya->set_datapoint_value(datapoint);
      - sensor.template.publish:
          id: kettle_target
          state: 95
      - switch.turn_on: the_relay
    turn_off_action:
      - switch.turn_off: the_relay
  
  
  - platform: template
    name: Kettle Heat To 100
    lambda: |-
      return (id(kettle_target).state == 100 && (id(tuya_15).state == 1 || id(tuya_15).state == 3) );
    turn_on_action:
      - switch.turn_off: the_relay
      - lambda: |-
          tuya::TuyaDatapoint datapoint{};
          datapoint.id = 4;
          datapoint.type = tuya::TuyaDatapointType::ENUM;
          datapoint.value_enum = 0x4;
          tuya_tuya->set_datapoint_value(datapoint);
      - sensor.template.publish:
          id: kettle_target
          state: 100
      - switch.turn_on: the_relay
    turn_off_action:
      - switch.turn_off: the_relay

sensor:
  - platform: template
    name: Kettle Target
    id: kettle_target
    unit_of_measurement: "°C"
    accuracy_decimals: 0
    
  - platform: tuya
    id: kettle_temp
    name: Kettle Temperature
    sensor_datapoint: 2
    unit_of_measurement: "°C"
    accuracy_decimals: 0
    filters:
      - filter_out: 0
      - or:
        - throttle: 30min
        - delta: 1
    on_raw_value: # We don't want filters to affect this
      then:
        - if:
            condition:
              lambda: |-
                return id(kettle_temp).raw_state == 0;
            then:
              - if:
                  condition:
                    text_sensor.state:
                      id: text_state
                      state: "Lifted"
                  else:
                    - text_sensor.template.publish:
                        id: text_state
                        state: "Lifted"
            else:
              - if:
                  condition:
                    text_sensor.state:
                      id: text_state
                      state: "Lifted"
                  then:
                    - text_sensor.template.publish:
                        id: text_state
                        state: "Ready"
    
  - platform: tuya
    id: tuya_15
    sensor_datapoint: 15
    on_value:
      then:
        - if:
            condition:
              lambda: |-
                return id(kettle_temp).raw_state == 0;
            else:
              - text_sensor.template.publish:
                  id: text_state
                  state: !lambda |-
                    switch(static_cast<int>(id(tuya_15).state)) {
                      case 0: return {"Ready"};
                      case 1: return {"Heating"};
                      case 2: return {"Done"};
                      case 3: return {"Keep warm"};
                      default: return {""};
                    }

  - platform: tuya
    id: tuya_4
    sensor_datapoint: 4
    on_value:
      then:
      - sensor.template.publish:
          id: kettle_target
          state: !lambda |-
            switch(static_cast<int>(id(tuya_4).state)) {
              case 0: return 80;
              case 1: return 85;
              case 2: return 90;
              case 3: return 95;
              case 4: return 100;
              default: return 0;
            }
7 Likes

Thanks, @simondotau! It’s remarkable: I was just looking for information about how to properly configure Tuya devices in ESPHome with a view to setting up this very kettle, and then I found your post.

One note (to help anyone finding this in the future) is that the lambdas for the turn_on_action in your Heat to… template switches is different, as the way to set Tuya datapoints in the ESPHome API has changed. I learned this from what I assume is a post of yours on a separate issue on Github…

So, for example, the Heat to 80 lambda turn_on_action is now:

  - platform: template
    name: Kettle heat to 80
 […]
    turn_on_action:
      - switch.turn_off: kettle_relay
      - lambda: |-
          tuya_tuya->set_datapoint_value(4, 0x0);
      - sensor.template.publish:
          id: kettle_target
          state: 80
[and continuing as before]

The first argument in the set_datapoint_value call is the datapoint ID (always 4 in this case), and the second is the value (so, 0x0 for 80º, 0x1 for 85º, etc.).

Thank you again! I’m going to see if I can do the same over the next few days for the Kogan kettle, as I like the idea of doing all the automation on-device, and I find this easier in ESPHome than Tasmota.

1 Like

Yep, that was me.

One thing I’m a bit frustrated with is that the kettle has a delay of around three seconds after been woken up (or the kettle being returned after filling with water) before the physical buttons will respond to input. Yet the kettle does respond immediately to commands from the ESP chip, so I can’t imagine that the delay is intentional or necessary. I don’t know if this behaviour is inherent to the controller or if it’s waiting for a response from the ESP chip which I’m not sending it.

Sadly I don’t have an unflashed kettle to compare with.

Hi and thanks for your work on this. I previously had my kettle running with tasmota. It kept going unavailable a few times a day and the beeping was annoying the crap out of me. A few weeks ago I switched it over to esphome using this guide and it has been perfect since.

Unfortunately I hate to be the bearer of bad news but I tried to update my firmware today using the new 2021.8.0 release of esphome and it gives an error. So I guess anyone using the update won’t be able to compile the binary.

Mines still working fine. But I have no idea on what the screen is telling me

I

The error is because the tuya_tuya->set_datapoint_value() function has been broken up in the most recent version of ESPHome.

Now, everywhere you see tuya_tuya->set_datapoint_value(, change it to tuya_tuya->set_enum_datapoint_value(.

You can also change the actual Enum value in the function (what comes after the comma in the bracketed portion, e.g. 0x3) to remove the 0x (so, in this case, 0x3 just becomes 3).

Thanks again. Worked a treat.

I’ve had a further play with this code to bring in the select functionality which was introduced in ESPHome 2021.8. In case anyone is interested, this is the meat of it:

packages:
  common: !include common/common.yaml

esphome:
  name: $device
  platform: ESP8266
  board: esp01_1m

# Disable logging over serial for Tuya UART
logger:
  baud_rate: 0

uart:
  rx_pin: GPIO13
  tx_pin: GPIO15
  baud_rate: 9600

tuya:

text_sensor:
  - platform: template
    name: "$name: status"
    id: text_state

select:
  - platform: template
    name: "$name: set temperature"
    id: kettle_control
    update_interval: 5s
    options:
      - 80º
      - 85º
      - 90º
      - 95º
      - 100º
    lambda: |-
      return to_string(int(id(kettle_target).state)) + "º";
    set_action:
      - lambda: |-
          if (strcmp(x.c_str(), "80º") == 0) {
            tuya_tuya->set_enum_datapoint_value(4, 0);
          } else if (strcmp(x.c_str(), "85º") == 0) {
            tuya_tuya->set_enum_datapoint_value(4, 1);
          } else if (strcmp(x.c_str(), "90º") == 0) {
            tuya_tuya->set_enum_datapoint_value(4, 2);
          } else if (strcmp(x.c_str(), "95º") == 0) {
            tuya_tuya->set_enum_datapoint_value(4, 3);
          } else if (strcmp(x.c_str(), "100º") == 0) {
            tuya_tuya->set_enum_datapoint_value(4, 4);
          } else {
            tuya_tuya->set_enum_datapoint_value(4, 4);
          }

switch:
  - platform: template
    id: esp_activity
    optimistic: true

  - platform: tuya
    id: kettle_relay
    name: "$name"
    switch_datapoint: 1

  - platform: tuya
    name: "$name: keep warm"
    switch_datapoint: 13

sensor:

  - platform: template
    name: "$name: target temperature"
    id: kettle_target
    unit_of_measurement: "°"
    accuracy_decimals: 0
    on_value:
      then:
        - lambda: |-
            std::string option = to_string(int(x)) + "º";
            auto call = id(kettle_control).make_call();
            call.set_option(option);
            call.perform();

  - platform: tuya
    id: kettle_temp
    name: "$name: current temperature"
    sensor_datapoint: 2
    unit_of_measurement: "°"
    accuracy_decimals: 0
    filters:
      - filter_out: 0
      - or:
        - throttle: 10min
        - delta: 1
    on_raw_value: # We don't want filters to affect this
      then:
        - if:
            condition:
              lambda: |-
                return id(kettle_temp).raw_state == 0;
            then:
              - if:
                  condition:
                    text_sensor.state:
                      id: text_state
                      state: "Lifted"
                  else:
                    - text_sensor.template.publish:
                        id: text_state
                        state: "Lifted"
            else:
              - if:
                  condition:
                    text_sensor.state:
                      id: text_state
                      state: "Lifted"
                  then:
                    - text_sensor.template.publish:
                        id: text_state
                        state: "Ready"

  - platform: tuya
    id: tuya_15
    sensor_datapoint: 15
    on_value:
      then:
        - if:
            condition:
              lambda: |-
                return id(kettle_temp).raw_state == 0;
            else:
              - text_sensor.template.publish:
                  id: text_state
                  state: !lambda |-
                    switch(static_cast<int>(id(tuya_15).state)) {
                      case 0: return {"Ready"};
                      case 1: return {"Heating"};
                      case 2: return {"Done"};
                      case 3: return {"Keep warm"};
                      default: return {""};
                    }

  - platform: tuya
    id: tuya_4
    sensor_datapoint: 4
    on_value:
      then:
      - sensor.template.publish:
          id: kettle_target
          state: !lambda |-
            switch(static_cast<int>(id(tuya_4).state)) {
              case 0: return 80;
              case 1: return 85;
              case 2: return 90;
              case 3: return 95;
              case 4: return 100;
              default: return 0;
            }

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: Australia/Sydney

@simondotau I’m very much just building on your work here! I dare say it could be simplified further, and I’ll be the first to admit that the logic is a bit hacky. But I do like being able just to turn on the kettle and it will heat to whatever the last-selected temperature was.

2 Likes

HI All,

Does anyone know if this config would work with the Kogan smart kettle?

Cheers

No, it doesn’t. I’ve experimented with making an ESPHome config for the Kogan kettle, but had limited success. Tasmota works fine, though.

@LiquidSunset Thanks mate, that’s what im using now but slowly migrating everything to ESPHome.
I had all my bulbs etc on Tasmota but a while ago everything broke and i couldn’t sent colour commands to them.

Cheers for the reply

I might test it out over the next few days. Thanks for the update.

Nice job @LiquidSunset!
I did much of the DPID bashing for the kettle that you will find on the Tasmota Template Repository.
I’ve been making the migration from Tasmota to ESPhome over the last couple of weeks.
It was very cool to find a working ESPhome config straight out of the box.

Thank you.

Jamie

That’s kind, @The_Duff: my own work on this kettle absolutely depended on that done by @simondotau and by you. So we’re all working together ultimately! :wink:

Thank you to @simondotau too then.
Awesome work fellas!