ESP smart сoffee machine Philips 5400 Series

I connected the 4 wire to GPIO14 and used this binary sensor, but it didn’t show anything, and there is no information in the logs, there is no message that the sensor saw something

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO14
      mode:
        input: true
    name: "Sensor WakeUP"
    on_press:
      then:
        - logger.log: "WakeUP signal detected"

Next, I decided to use the APS sensor, which also did not see anything, which is quite expected, since taking readings through an oscilloscope I saw noise and voltage up to 300 mv. After breaking the 4 wire, the coffee machine turns on quietly.

sensor:
  - platform: adc
    pin: GPIO39
    id: isVoltage
    name: "Voltage"
    update_interval: 500ms
    accuracy_decimals: 7
    attenuation: auto

[17:00:13][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:00:15][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:00:16][D][sensor:094]: ‘Voltage’: Sending state 0.08400 V with 2 decimals of accuracy
[17:00:17][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:00:18][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:00:19][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:00:20][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:00:36][D][sensor:094]: ‘Voltage’: Sending state 0.90200 V with 2 decimals of accuracy
[17:00:37][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 2 decimals of accuracy
[17:04:59][D][sensor:094]: ‘Voltage’: Sending state 0.07500 V with 7 decimals of accuracy

Measuring 4 wires through a logic analyzer




It’s not a fact that the 4 wire is a Wake UP, since the scheme was drawn by an amateur radio enthusiast for a coffee machine of another series, not for the 5400.

As a result, I soldered the wire to the resistor R110 and connected it to an 8-channel zigbee relay for testing. It works well, I can turn it on and off, but the on and off button itself does not work while the wire is in the relay. If I disconnect the wiring from the relay, the button on the control panel works. To make it work, you need to take GND, you can from ESP, you can from a coffee machine, no difference, connect a capacitor from 10 to 47 pf to GND and connect it all to a relay that will short and open GND.



After a successful experiment, I ordered such a relay. The module is based on the AXICOM IM01 3vdc SMD electromechanical relay from TE. The control voltage is 3 Volts. Plug-in load up to 2A, 250V AC. It can be used in various projects, including on Arduino, where it is required to switch the voltage, for example, to include a load of 220 volts.

1 Like

Ive found a similar project that uses uart and reads the frames, this should give you some idea of how to read the data, and build the frames
https://github.com/joshbenner/esphome-daikin-s21/blob/71b210be27e1ca6f5e7f202f390cc89f8275bfac/components/daikin_s21/s21.cpp

if you scroll down to line 194, you will see how they decode. ive added comments to the relevant parts below

bool DaikinS21::read_frame(std::vector<uint8_t> &payload) {
  uint8_t byte;
  std::vector<uint8_t> bytes;
  uint32_t start = millis();
  bool reading = false;
  while (true) {
    if (millis() - start > S21_RESPONSE_TIMEOUT) {
      ESP_LOGW(TAG, "Timeout waiting for frame");
      return false;
    }
    while (this->rx_uart->available()) {
      this->rx_uart->read_byte(&byte);
      if (byte == ACK) {
        ESP_LOGW(TAG, "Unexpected ACK waiting to read start of frame");
        continue;
      } else if (!reading && byte != STX) {
        ESP_LOGW(TAG, "Unexpected byte waiting to read start of frame: %x",
                 byte);
        continue;
      } else if (byte == STX) {  // this is the start of frame byte, would need to be adapted to suit the 3x AA bytes in the coffee machine frame
        reading = true;
        continue;
      }
      if (byte == ETX) { //ETX is the stop byte / end of frame byte
        reading = false;  // once we get stop byte it sets this false so it can exit the loop later
        uint8_t frame_csum = bytes[bytes.size() - 1];  // read the last byte as its the checksum, would need to be adapted to suit the 4x bytes in the coffee machine checksum
        bytes.pop_back();  // removes the checksum byte from all the bytes received, once again, will need to adjust for 4 bytes not 1
        uint8_t calc_csum = s21_checksum(bytes); // create its own checksum based on the bytes array
        if (calc_csum != frame_csum) { // compares received checksum against own calcultaed one
          ESP_LOGW(TAG, "Checksum mismatch: %x (frame) != %x (calc from %s)",
                   frame_csum, calc_csum,
                   hex_repr(&bytes[0], bytes.size()).c_str());
          return false;
        }
        break;
      }
      bytes.push_back(byte); // add the received byte to the bytes array
    }
    if (bytes.size() && !reading) // once we have some bytes and reading the frame has finished we leave the while loop
      break;
    yield();  
  }
  payload.assign(bytes.begin(), bytes.end()); // this sets the received bytes (without header, crc and end byte) in the payload address.
  return true;
}

I will try to delve into the essence of this topic, but I do not understand how to insert it into ESPHome, so I do not know the C++ programming language and I am not a programmer, but an amateur and more often copy ready-made versions than I write myself

Ive changed the lambda up a bit to hopefully help discover more of the status info and hopefully simplify things.

ive removed alot of the info you tracked down, just to test if the lambda part works with the bit checks for the water container and pallet. it should spit out the part of the message without the header, checksum and stop bit into B0 meesage and b5 message

if you put the machine in maintenance mode and test the different components in isolation it should help decode the messages.

substitutions:
  board_name: ESP32 Smart Coffee Philips
  node_name: esp32-smart-coffee-philips

esphome:
  name: ${node_name}
  friendly_name: esp32-smart-coffee-philips
  comment: ESP32 Smart Coffee Philips

esp32:
  board: esp32dev
  framework:
    type: arduino

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: on
  reboot_timeout: 10min
  ap:
    ssid: ESP SmartCoffeePhilips
    password: !secret ap_esp_password

captive_portal:

web_server:
  port: 80

logger:
  level: DEBUG
  baud_rate: 0

ota:
  password: "esphome"

api:
  encryption:
    key: !secret api_key


external_components:
  - source: github://TillFleisch/ESPHome-Philips-Smart-Coffee@main


uart:
 - id: uart_mainboard
   rx_pin: GPIO3
   tx_pin: GPIO1
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: RX
     dummy_receiver: false


 - id: uart_display
   rx_pin: GPIO16
   tx_pin: GPIO17
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: RX
     dummy_receiver: false
     sequence: 
      - lambda: |-
          UARTDebug::log_hex(direction, bytes, ':');
          if (bytes[0]==0xAA && bytes[1]==0xAA && bytes[2]==0xAA){ // header
            if (bytes[3]==0xB0) { // status message
              id(b0message).publish_state("B0 %x %x %x %x %x %x %x %x %x", bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12]);
              if (bytes[9] & 0b1000000) ? id(idPallet).publish_state("Removed") : id(idPallet).publish_state("Installed");
              if (bytes[9] & 0b0100000) ? id(idWater).publish_state("Removed") : id(idWater).publish_state("Installed");
            }
            if (bytes[3]==0xB5) { // status2 message
              id(b5message).publish_state("B5 %x %x %x %x %x %x %x %x", bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11]);
            }
          }

philips_series_5400:
  display_uart: uart_display
  mainboard_uart: uart_mainboard
  power_pin: GPIO12
  id: philip



#####################################################################################
################################### Text Sensor ################################
text_sensor:
  - platform: wifi_info
    ip_address:
      name: ${board_name} IP

  - platform: template
    name: "Water"
    id: idWater
    update_interval: 60s

  - platform: template
    name: "Coffee Grounds Container"
    id: idCoffeeGroundsContainer
    update_interval: 60s

  - platform: template
    name: "Coffee Pallet"
    id: idPallet
    update_interval: 60s

  - platform: template
    name: "Status 1"
    id: idStatusUnknown1
    update_interval: 60s

  - platform: template
    name: "Status 2"
    id: idStatusUnknown2
    update_interval: 60s

  - platform: template
    name: "Making Coffee"
    id: idMakingCoffee
    update_interval: 10s

  - platform: template
    name: "Grain Tray"
    id: idGrainTray
    update_interval: 60s

  - platform: template
    id: idSystemStatus
    name: "System Status"
    update_interval: 60s

  - platform: template
    id: b0message
    name: "B0 message"
    update_interval: 10s

  - platform: template
    id: b5message
    name: "B5 message"
    update_interval: 10s

button:
  - platform: restart
    name: Restart
    icon: mdi:restart

  - platform: template
    name: "Turn off" 
    on_press:
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0xFE, 0x00, 0x00, 0xC8, 0x87, 0x1B, 0x40, 0x55] #AA:AA:AA:FE:00:00:C8:87:1B:40:55


#####################################################################################
############################ Coffee Recipes #############################

#Drink: Coffee X1. Grain: Maximum. Coffee: 150 ml. Serving: 1
  - platform: template
    name: "Coffee Max 150ml" 
    on_press:
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x93, 0x05, 0x01, 0x01, 0x6A, 0x52, 0x66, 0xE6, 0x55]
      - uart.write:    
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x90, 0x06, 0x0A, 0x00, 0x02, 0x00, 0x02, 0x02, 0x00, 0x78, 0x00, 0x00, 0x00, 0xFC, 0x28, 0xA1, 0x4C, 0x55]
      - uart.write:    
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x91, 0x07, 0x01, 0x03, 0xA3, 0x2F, 0xE5, 0xA1, 0x55]


#Drink: Cappuccino. Grain: Minimum (2 out of 5 level scale). Coffee: 20 ml. Milk 100 ml.
  - platform: template
    name: "Cappuccino Min 20/100ml" 
    on_press:
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x93, 0x05, 0x01, 0x01, 0x6A, 0x52, 0x66, 0xE6, 0x55]
      - uart.write:    
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x90, 0x06, 0x0A, 0x03, 0x01, 0x00, 0x02, 0x03, 0x02, 0x14, 0x00, 0x64, 0x00, 0xE7, 0xC9, 0xE6, 0x5F, 0x55]
      - uart.write:    
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x91, 0x07, 0x01, 0x03, 0xA3, 0x2F, 0xE5, 0xA1, 0x55]


#Drink: Cappuccino. Grain: Maximum. Coffee: 60 ml. Milk: 150 ml
  - platform: template
    name: "Cappuccino Max 60/150ml" 
    on_press:
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x93, 0x05, 0x01, 0x01, 0x6A, 0x52, 0x66, 0xE6, 0x55]
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x90, 0x06, 0x0A, 0x03, 0x02, 0x00, 0x02, 0x03, 0x02, 0x3C, 0x00, 0x96, 0x00, 0xCC, 0xF1, 0x67, 0x25, 0x55]
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x91, 0x07, 0x01, 0x03, 0xA3, 0x2F, 0xE5, 0xA1, 0x55]

1 Like

I copied your example and there were no errors during the check, but when I started sending the updated code to esp, the following errors occurred during compilation

hmm thats my miss understanding of the text sensor needing static strings… lets remove that for a minute and try get the bit checks part working

try this, sorry i’m travelling at the moment so cant test myself

      - lambda: |-
          UARTDebug::log_hex(direction, bytes, ':');
          if (bytes[0]==0xAA && bytes[1]==0xAA && bytes[2]==0xAA){ // header
            if (bytes[3]==0xB0) { // status message
              if (bytes[9] & (1 << 7)) ? id(idPallet).publish_state("Removed") : id(idPallet).publish_state("Installed");
              if (bytes[9] & (1 << 6)) ? id(idWater).publish_state("Removed") : id(idWater).publish_state("Installed");
            }
          }

I can’t compile, an error occurs during compilation. It will be right if you can check the correctness of the code yourself during compilation, and then publish the working code here

I implemented switching the coffee machine on and off by means of a relay. It works great. I took a single-channel 5V relay (KY-019), you can use the relay that I suggested above, but it hasn’t arrived yet, and it arrived much earlier than AXICOM IM01 3vdc SMD

I glued the relay itself to the back of the control panel housing, made a divorce for the servo, for esp and for the relay.

I also implemented a water drain to drain water when flushing the coffee machine past the cup, and then you can pour coffee into the cup.

Watch the video how the water drain works and how the full process works, from turning on to making coffee

Connection diagram

You can download the part for printing on a 3d printer here

Important. I designed the hole for the gear for the SG90 Micro Servo. It comes in very tight, but it holds tight and does not jump off. There is no need to expand the hole, just make a good effort when pressing the servo into the hole and the servo will start to enter.

You can set the desired position of the servo to adjust the water discharge. I have set the following values
image

Code for turning the coffee machine on and off, as well as servo control
substitutions:
  board_name: Coffee Philips 5400
  node_name: coffee-philips-5400

esphome:
  name: ${node_name}
  friendly_name: coffee-philips-5400
  comment: ESP32 Coffee Philips 5400

esp32:
  board: esp32dev
  framework:
    type: arduino


wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: on
  reboot_timeout: 10min

  ap:
    ssid: ESP SmartCoffeePhilips
    password: !secret ap_esp_password


captive_portal:

web_server:
  port: 80

logger:
  level: DEBUG
  baud_rate: 0


ota:
  password: "esphome"


api:
  encryption:
    key: !secret api_key
  services:
   - service: control_servo
     variables:
       level: float
     then:
       - servo.write:
           id: servo_control
           level: !lambda 'return level / 100.0;'
       - sensor.template.publish:
           id: servo_sensor
           state: !lambda 'return (float)level;'

#####################################################################################
################################## External component ################################
external_components:
  - source: github://TillFleisch/ESPHome-Philips-Smart-Coffee@main


#####################################################################################
######################################### UART ######################################
uart:
 - id: uart_display
   rx_pin: GPIO16
   tx_pin: GPIO17
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: RX
     dummy_receiver: false

 - id: uart_mainboard
   rx_pin: GPIO3
   tx_pin: GPIO1
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: RX
     dummy_receiver: false
     sequence: 
      - lambda: |-
          UARTDebug::log_hex(direction, bytes, ':');
          //AA:AA:AA:B0
          if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x06 && bytes[9]==0x00) { 
            id(idWater).publish_state("There is water");
            id(idPallet).publish_state("Inserted");
            id(idGrainTray).publish_state("There are grains");
            id(idStatus).publish_state("Choose a drink");
            id(idCoffeeGroundsContainer).publish_state("Empty");
            }
          //AA:AA:AA:B0 "Availability of coffee beans"
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x08 && bytes[7]==0x05) { id(idGrainTray).publish_state("The grains have run out"); }

          //AA:AA:AA:B0 "Took out the container with water"
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x0E && bytes[9]==0x40) { id(idWater).publish_state("There is no water"); }

          //AA:AA:AA:B0 "Removed the pallet"
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x0E && bytes[9]==0x80) { id(idPallet).publish_state("Retrieved"); }

          //AA:AA:AA:B0 "The container with coffee grounds is filled"
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x0E && bytes[9]==0x00) { id(idCoffeeGroundsContainer).publish_state("Oporozhe. container for coffee grounds"); }

          //AA:AA:AA:B0 "The pallet and the container with water were taken out"
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x0E && bytes[9]==0xC0) { 
            id(idWater).publish_state("There is no water"); 
            id(idPallet).publish_state("Retrieved");
            }
          
          //AA:AA:AA:B0 "Statuses"
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x0C && bytes[7]==0x01) { id(idStatus).publish_state("Enjoy"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x0C && bytes[7]==0x02) { id(idStatus).publish_state("Something (07 0C 02)"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x01 && bytes[7]==0x00) { id(idStatus).publish_state("Something (07 01 00)"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x05 && bytes[7]==0x00) { id(idStatus).publish_state("Off"); } 
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x0E) { id(idStatus).publish_state("Water heating"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x0D) { id(idStatus).publish_state("Grinding the grains"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x10) { id(idStatus).publish_state("Pour the milk"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x11) { id(idStatus).publish_state("Pour the coffee"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x12) { id(idStatus).publish_state("Pre-dosing of water"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x13) { id(idStatus).publish_state("Creating steam for milk"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x14) { id(idStatus).publish_state("The brewing unit to the brewing position"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x07 && bytes[7]==0x15) { id(idStatus).publish_state("Enjoy"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x08 && bytes[7]==0x0E) { id(idStatus).publish_state("Heating"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x08 && bytes[7]==0x02) { id(idStatus).publish_state("Flushing"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB0 && bytes[5]==0x07 && bytes[6]==0x08 && bytes[7]==0x14) { id(idStatus).publish_state("Something (07 08 14)"); }

          //AA:AA:AA:B5 "Error Code"
          if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 && bytes[10]==0x00 && bytes[11]==0x00) { id(idErrorCode).publish_state("00"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 && bytes[10]==0x00 && bytes[11]==0x0B) { id(idErrorCode).publish_state("0B"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 && bytes[10]==0x00 && bytes[11]==0xE6) { id(idErrorCode).publish_state("E6"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 && bytes[10]==0x00 && bytes[11]==0x80) { id(idErrorCode).publish_state("80"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 && bytes[10]==0x00 && bytes[11]==0xCB) { id(idErrorCode).publish_state("CB"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 &&& bytes[10]==0x00 & bytes[11]==0xFF) { id(idErrorCode).publish_state("FF"); }
          else if (bytes[0]==0xAA && bytes[3]==0xB5 && bytes[5]==0x06 && bytes[10]==0x00 && bytes[11]==0xA0) { id(idErrorCode).publish_state("A0"); }

          if (bytes[0]==0xAA) { id(idPowerStatus).publish_state(true); }



#####################################################################################
############################## Global variables #####################################
globals:
  - id: saved_position
    type: int
    initial_value: '0'
    restore_value: true


#####################################################################################
############################### Output Platform #####################################
output:
  - platform: ledc
    id: pwm_output
    pin: GPIO27
    frequency: 50 Hz

#####################################################################################
################################ Configuration ######################################
servo:
  - id: servo_control
    output: pwm_output
    auto_detach_time: 5s
    transition_length: 5s
    restore: true


#####################################################################################
################################## Connected component ##############################

philips_series_2200:
  display_uart: uart_display
  mainboard_uart: uart_mainboard
  power_pin: GPIO12
  id: philip

#####################################################################################
##################################### Text Sensor ###################################
text_sensor:
  - platform: wifi_info
    ip_address:
      name: IP

  - platform: template
    name: "Water"
    icon: mdi:cup-water
    id: idWater
    update_interval: 60s

  - platform: template
    name: "Coffee Grounds Container"
    icon: mdi:train-car-centerbeam-full
    id: idCoffeeGroundsContainer
    update_interval: 60s

  - platform: template
    name: "Coffee Pallet"
    icon: mdi:spirit-level
    id: idPallet
    update_interval: 60s

  - platform: template
    name: "Error Code"
    icon: mdi:alert-circle-outline
    id: idErrorCode
    update_interval: 60s

  - platform: template
    name: "Grain Tray"
    icon: mdi:grain
    id: idGrainTray
    update_interval: 60s

  - platform: template
    name: "Status"
    icon: mdi:coffee-to-go
    id: idStatus
    update_interval: 60s


#####################################################################################
################################### Binary sensor ###################################
binary_sensor:
  - platform: template
    name: "PowerStatus"
    id: idPowerStatus
    lambda: |-
      if (id(idPowerStatus).state) {
        return true;
      } else {
        return false;
      }


#####################################################################################
################################## Sensor ###########################################
sensor:
#WiFi signal strength sensor
  - platform: wifi_signal
    name: RSSI WiFi
    icon: mdi:wifi
    update_interval: 60s

#Servomotor position sensor
  - platform: template
    name: Servo Sensor Position
    id: servo_sensor
    icon: mdi:valve
    on_value:
      then:
        - script.execute: script_servo
        - script.execute: record_servo_position


#####################################################################################
#################################### Switch #########################################
switch:
  - platform: gpio
    pin: GPIO14
    name: "Relay Switch"
    id: idRelaySwitch
    internal: True #Скрыть - true \показать - false
    restore_mode: ALWAYS_OFF

#Выключатель сервопривода с начальной и конечной позицией
  - platform: template
    id: servo_control_switch
    name: Valve open/closed
    icon: mdi:coffee-maker-outline
    restore_state: false
    turn_on_action:
      - sensor.template.publish:
          id: servo_sensor
          state: !lambda 'return id(valve_open).state;'
      - switch.template.publish:
         id: servo_control_switch
         state: true
      - servo.write:
         id: servo_control
         level: !lambda 'return (id(valve_open).state/100);'
    turn_off_action:
      - sensor.template.publish:
          id: servo_sensor
          state: !lambda 'return id(valve_closed).state;'
      - switch.template.publish:
         id: servo_control_switch
         state: false
      - servo.write:
         id: servo_control
         level: !lambda 'return (id(valve_closed).state/100);'


#####################################################################################
###################################### Number #######################################
#Slider for servo control
number:
  - platform: template
    name: Valve Position
    min_value: -100
    max_value: 100
    update_interval: 1s 
    mode: slider #slider/box
    lambda: 'return id(servo_sensor).state;' 
    step: 1
    set_action:
      then:
        - servo.write:
           id: servo_control
           level: !lambda 'return x / 100.0;'
        - sensor.template.publish:
           id: servo_sensor
           state: !lambda 'return x;'

#Specify the initial position of the servo
  - platform: template
    name: Valve open
    id: valve_open
    min_value: -100
    max_value: 100
    mode: box #slider/box
    step: 1
    optimistic: true
    restore_value: true

#Specify the final position of the servo
  - platform: template
    name: Valve closed
    id: valve_closed
    min_value: -100
    max_value: 100
    mode: box #slider/box
    step: 1
    optimistic: true
    restore_value: true



#####################################################################################
####################################### Script ######################################
script:
  - id: script_servo
    then:
      - if:
          condition:
            - lambda: 'return id(servo_sensor).state == id(valve_open).state;'
          then:
            - sensor.template.publish:
               id: servo_sensor
               state: !lambda 'return (id(valve_open).state/100);'
            - switch.turn_on: servo_control_switch
      - if:
          condition:
            and:
              - lambda: 'return id(servo_sensor).state == id(valve_closed).state;'
          then:
            - sensor.template.publish:
               id: servo_sensor
               state: !lambda 'return id(valve_closed).state;'
            - switch.turn_off: servo_control_switch

  - id: record_servo_position
    then:
      - globals.set:
          id: saved_position
          value: !lambda 'return id(servo_sensor).state;'


#####################################################################################
####################################### Button ######################################
button:
#Restart
  - platform: restart
    name: Restart
    icon: mdi:restart

#Turn on the coffee machine
  - platform: template
    name: "Power ON"
    icon: mdi:power
    on_press:
     - switch.turn_on: idRelaySwitch
     - delay: 1s
     - switch.turn_off: idRelaySwitch

#Turn off the coffee machine
  - platform: template
    name: "Power OFF"
    icon: mdi:power
    on_press:
     - switch.turn_on: idRelaySwitch
     - delay: 2s
     - switch.turn_off: idRelaySwitch




#####################################################################################
####################################### Time #######################################
time:
  - platform: sntp
    id: sntp_time
    timezone: Europe/Moscow
    on_time:
      - seconds: 59
        then:
          - lambda: |-
              id(idPowerStatus).publish_state(false);

I’ll post another option. It is possible to implement switching on and off via the PC817C optocoupler, but with a relay it is better. If you implement the connection via an optocoupler, then by de-energizing and turning on the ESP, we give an impulse to the coffee machines and the coffee machine will turn on, and in the case of a relay, this will not happen. You can safely de-energize and plug in the coffee machine and the coffee machine does not turn on, also the coffee machine does not turn on if there is a short-term power outage in the house. Any capacitor will do, 10/20/47pf.

image

@chris.huitema

If you use this option, it works. For reference, I used ChatGPT, so there may be errors in the code from the programmer’s point of view

 - id: uart_mainboard
   rx_pin: GPIO3
   tx_pin: GPIO1
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: RX
     dummy_receiver: false
     sequence: 
      - lambda: |-
          UARTDebug::log_hex(direction, bytes, ':');
          if (bytes[0]==0xAA && bytes[1]==0xAA && bytes[2]==0xAA){ // header
            if (bytes[3]==0xB0) { // status message
              std::string b0_message = "B0 ";
              for (int i = 4; i <= 12; i++) {
                char hex_str[3];
                snprintf(hex_str, sizeof(hex_str), "%02X", bytes[i]);
                b0_message += hex_str;
                b0_message += " ";
              }
              id(b0message).publish_state(b0_message);
            }
            if (bytes[3]==0xB5) { // status2 message
              std::string b5_message = "B5 ";
              for (int i = 4; i <= 11; i++) {
                char hex_str[3];
                snprintf(hex_str, sizeof(hex_str), "%02X", bytes[i]);
                b5_message += hex_str;
                b5_message += " ";
              }
              id(b5message).publish_state(b5_message);
            }
            if (bytes[15]==0x90) { // status message
              std::string message90 = "90 ";
              for (int i = 18; i <= 27; i++) {
                char hex_str[3];
                snprintf(hex_str, sizeof(hex_str), "%02X", bytes[i]);
                message90 += hex_str;
                message90 += " ";
              }
              id(id90message).publish_state(message90);  
            }
          }

and it works that way too

          if (bytes[0]==0xAA && bytes[1]==0xAA && bytes[2]==0xAA){ // header
            if (bytes[3]==0xB0) { // status message
              std::string b0_message = "B0 " + std::to_string(bytes[4]) + " " + std::to_string(bytes[5]) + " " + std::to_string(bytes[6]) + " " +
                                      std::to_string(bytes[7]) + " " + std::to_string(bytes[8]) + " " + std::to_string(bytes[9]) + " " +
                                      std::to_string(bytes[10]) + " " + std::to_string(bytes[11]) + " " + std::to_string(bytes[12]);
              id(b0message).publish_state(b0_message);
            }
            if (bytes[3]==0xB5) { // status2 message
              std::string b5_message = "B5 " + std::to_string(bytes[4]) + " " + std::to_string(bytes[5]) + " " +
                                      std::to_string(bytes[6]) + " " + std::to_string(bytes[7]) + " " +
                                      std::to_string(bytes[8]) + " " + std::to_string(bytes[9]) + " " +
                                      std::to_string(bytes[10]) + " " + std::to_string(bytes[11]);
              id(b5message).publish_state(b5_message);
            }
            if (bytes[15]==0x90) { // status message
              std::string message90 = "90 " + std::to_string(bytes[18]) + " " + std::to_string(bytes[19]) + " " +
                                      std::to_string(bytes[20]) + " " + std::to_string(bytes[21]) + " " +
                                      std::to_string(bytes[22]) + " " + std::to_string(bytes[23]) + " " +
                                      std::to_string(bytes[24]) + " " + std::to_string(bytes[25]) + " " +
                                      std::to_string(bytes[26]) + " " + std::to_string(bytes[27]);
              id(id90message).publish_state(message90);  
            }
          }

I rewrote the ESPHome-Philips-Smart-Coffee component for Philips 5400, removed all sensors, switches, buttons and everything that does not apply to Philips 5400, left only the UART. The Philips 5400 component can be downloaded here. Why did I take a component from ESPHome-Philips-Smart-Coffee? The thing is that if you use another uart_mitm component, then the coffee machine does not work correctly, it can turn off at any time, standby mode does not work normally, and if you use a component from ESPHome-Philips-Smart-Coffee, then the coffee machine works correctly. The coffee machine will not turn off exactly as long as the standby mode, which we set through the settings menu of the coffee machine, does not work, or until we turn it off ourselves. The component must be placed in the \esphome\components folder\

To operate the component for Philips 5400, you need to insert the code

external_components:
  - source:
      type: local
      path: components
    components: [philips_series_5400]

philips_series_5400:
  display_uart: uart_display
  mainboard_uart: uart_mainboard
  id: philip

I created sensors, and thanks to the sensors, I came to some understanding of how to run coffee recipes many times. It turns out that the counter is working and it is impossible to jump over the counter, the coffee recipe will not work. Here’s what I saw

The code for the operation of sensors message 91 and 93

In order for the Message 90 sensor to work, you need to enable debugging in both directions for uart_mainboard. It is not recommended to do this, because because of this, the changed coffee preparation parameters are not saved in the profile and interferes with the normal operation of the coffee machine, so I turned off BOTH and specified RX

 - id: uart_mainboard
   rx_pin: GPIO3
   tx_pin: GPIO1
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: RX
     dummy_receiver: false

uart:
 - id: uart_display
   rx_pin: GPIO16
   tx_pin: GPIO17
   baud_rate: 115200
   stop_bits: 1
   data_bits: 8
   parity: NONE
   rx_buffer_size: 256
   debug:
     direction: BOTH
     dummy_receiver: false
     sequence: 
      - lambda: |-
          UARTDebug::log_hex(direction, bytes, ':');
          //Coffee drinks
          //Message 93. Teaches bytes 0 through 11. We display in this form [0xAA, 0xAA, 0xAA, 0x93, 0x0F, 0x01, 0x01, 0xBC, 0xD7, 0xF1, 0xEB, 0x55]
          if (bytes[0] == 0xAA && bytes[2] == 0xAA && bytes[3] == 0x93) {
              std::string message93 = "[";
              for (int i = 0; i <= 11; i++) {
                  char hex_str[6];
                  snprintf(hex_str, sizeof(hex_str), "0x%02X", bytes[i]);
                  message93 += hex_str;
                  if (i != 11) {
                      message93 += ", ";
                  }
              }
              message93 += "]";
              id(id93message).publish_state(message93);
          }

          //Message 90. Accounting for bytes from 0 to 20. We display it in this form [0xAA, 0xAA, 0xAA, 0x90, 0x06, 0x0A, 0x00, 0x03, 0x00, 0x02, 0x03, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xC5, 0x47, 0xCB, 0x8E, 0x55]
          if (bytes[0] == 0xAA && bytes[2] == 0xAA && bytes[3] == 0x90) {
              std::string message90 = "[";
              for (int i = 0; i <= 20; i++) {
                  char hex_str[6];
                  snprintf(hex_str, sizeof(hex_str), "0x%02X", bytes[i]);
                  message90 += hex_str;
                  if (i != 20) {
                      message90 += ", ";
                  }
              }
              message90 += "]";
              id(id90message).publish_state(message90);
          }

          //Message 91.Accounting for bytes from 0 to 11. We display it in this form [0xAA, 0xAA, 0xAA, 0x91, 0x12, 0x01, 0x08, 0xB0, 0x97, 0xDA, 0x2C, 0x55]
          if (bytes[0] == 0xAA && bytes[2] == 0xAA && bytes[3] == 0x91) {
              std::string message91 = "[";
              for (int i = 0; i <= 11; i++) {
                  char hex_str[6];
                  snprintf(hex_str, sizeof(hex_str), "0x%02X", bytes[i]);
                  message91 += hex_str;
                  if (i != 11) {
                      message91 += ", ";
                  }
              }
              message91 += "]";
              id(id91message).publish_state(message91);  
          } 

text_sensor:
  - platform: template
    name: "Message1 93"
    id: id93message
    update_interval: 60s

  - platform: template
    name: "Message2 90"
    id: id90message
    update_interval: 60s

  - platform: template
    name: "Message3 91"
    id: id91message
    update_interval: 60s

To understand how the counter works, you can watch the video

Looking at the Message 90 and 93 sensors, I noticed the following.

This is what bytes look like when the coffee machine offers to choose a drink and before switching off

Turn off the coffee machine

The coffee machine is turned off

The coffee machine was completely de-energized and plugged into an outlet, after a pause of 5 seconds

The coffee machine was turned on, the heating and flushing stage began

The coffee machine has passed the washing stage and offers to choose a drink

I realized that immediately after the coffee machine turns on and offers to choose a drink, it is impossible to send a command to prepare a recipe, since the washing stage will start. To prevent this from happening, you need to wait until it happens like this


When making coffee drinks, the counter is triggered and I had an idea how to run the recipe, but I can’t write the code correctly. ChatGPT is trying to help me, but it doesn’t work well and it offers a bunch of options, and most of the options are wrong, compilation goes wrong. ChatGPT simply offers codes by brute force and there is zero sense from this.

The point is that we extract bytes from the Message 91 sensor and insert it as a variable to send a command, thus simulating the operation of the counter

For the test, I created a button and it works correctly. This is not sending a cooking command, but just checking to see how bytes are being sent. I extract bytes from the Message 91 sensor and send the Message Send button 91 to the sensor and the sensor displays exactly the same as in Message3 91
[0xAA, 0xAA, 0xAA, 0x91, 0x08, 0x01, 0x08, 0x16, 0xB1, 0x6B, 0x3D, 0x55]

  - platform: template
    name: "Message Button 91"
    icon: mdi:coffee
    on_press:
      - text_sensor.template.publish:
          id: idMessageButton
          state: !lambda 'return id(id91message).state;'

To send the command, I first tried this option, but it doesn’t work, because when compiling I get an error

button
  - platform: template
    name: "Coffee"
    icon: mdi:coffee
    on_press:
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x90, 0x06, 0x0A, 0x00, 0x03, 0x00, 0x02, 0x03, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xC5, 0x47, 0xCB, 0x8E, 0x55]
      - uart.write:
          id: uart_mainboard
          data: return 'id(id91message).state;'

And I got this code from ChatGPT, the compilation was successful, but it doesn’t work and I don’t see how bytes are being sent

button
  - platform: template
    name: "Coffee"
    icon: mdi:coffee
    on_press:
      - uart.write:
          id: uart_mainboard
          data: [0xAA, 0xAA, 0xAA, 0x90, 0x06, 0x0A, 0x00, 0x03, 0x00, 0x02, 0x03, 0x00, 0x1E, 0x00, 0x00, 0x00, 0xC5, 0x47, 0xCB, 0x8E, 0x55]
      - uart.write:
          id: uart_mainboard
          data: !lambda |-
            std::string message = id(id91message).state.c_str();
            std::vector<uint8_t> bytes;
            for (std::size_t i = 0; i < message.size(); i += 4) {
              std::string byte_str = message.substr(i, 4);
              uint8_t byte = std::stoi(byte_str, nullptr, 16);
              bytes.push_back(byte);
            }
            return bytes;

Table of analysis of the protocol AA AA AA 90 which is responsible for accounting. The last 4 bytes are the checksum. If you change one byte, the recipe will not work.

:partying_face:

Thanks to one person, you can now make coffee many times. We wrote a component to control the coffee machine. But I must say right away, the code is not perfect, but it works. You can use the code as it is or modify it and bring it to mind. A pass-through counter has been implemented in the code and now you can prepare coffee remotely as many times as you want

The project ESP-Philips-5400-Coffee-Machine is still in Russian, I will add a page in English soon.

The project itself ESP-Philips-5400-Coffee-Machine

In the config you need to add

external_components:
  - source:
      type: git
      url: https://github.com/DivanX10/ESP-Philips-5400-Coffee-Machine


philips_series_5400:
  display_uart: uart_display
  mainboard_uart: uart_mainboard
  id: philip

The full code can be found here

1 Like

I have one of these coffee machines and this project intrigues me. Amazing work reverse engineering the machine!

What is the use case for this, though? What functionality does this give over just the buttons?

It all depends on you.

  1. The coffee machine can be turned on and off remotely
  2. There are sensors showing:
  • Availability of water
  • Availability of grain
  • Is the pallet inserted
  • Is the coffee grounds container empty
  • Coffee preparation statuses
  • Power status, whether the coffee machine is turned on or not
  1. You can send your recipes for making coffee based on the recipe table, or you can make coffee and the recipe will be displayed in the coffee preparation sensor and it can be used

The coffee machine makes me coffee as I wake up, it’s damn cool, it’s what I dreamed of. For my philips 5400 coffee machine, the REDMOND SkyCoffee M1519S coffee maker worked like this. It turned on when we woke up, but with the advent of the coffee machine I wanted to do the same and I did it.

Video of how the coffee machine turns on and makes me coffee

The: make coffee as you wake up is nice, but doesn’t it still need to do the first cleaning after being off for a long time? Or does this make is so that you can skip that step?

Do you mean when the coffee machine turns on and does the heating and rinsing and drains the water into the cup?

To do this, I designed a water outlet. When the coffee machine is turned on, it starts the washing stage and water flows through it into the cup if it is standing or is poured into the tray if there is no cup. The water drain removes water from the cup, preventing water from entering the cup during washing and the cup remains dry, all the washing water goes past the cup


1 Like

That is very clever

News
The component for controlling the Philips 5400 coffee machine is completely rewritten and ready. I added an English version to the coffee machine project here. Any improvements and improvements in the code are welcome. Fork and finish the code at your discretion. Share a link to your projects with revision

This is how the control panel of the coffee machine and the panel of parameters for making coffee drinks looks like

image

1 Like

Is the relay specifically if you want control over on/off? What is the use-case for using the optocoupler instead?

Optocoupler is probably a good option. Why am I writing probably? Because I do not know how the optocoupler will behave if connected to a pin without pulses when power is supplied. As I later found out, the esp does not have all pins free (without pulses when current is applied). When I connected the optocoupler to the esp, I didn’t think about it and when I plugged the coffee machine into the socket, the coffee machine turned on every time. This does not happen with the relay. The relay can be connected to any pin on the ESP and the coffee machine will not turn on when plugged into the socket. When I remembered about it, by this time I had everything assembled on the relay and had no desire to check. But, in the case of an optocoupler, you can turn on the coffee machine from the button on the control panel and remotely, but it does not work with the relay, only switching on with esp.

1 Like