Panasonic Aquarea Heatpump with ESPHome (Heishamon)

Hi all,

Owner/user of a heatpump from Panasonic (Aquarea) probably already know that there are some ways how to integrate their heatpump into Home Assistant. The most known are probably heishamon and the Aquarea Smart Cloud Integration.

Since I have already the Panasonic WiFi-Module CZ-TAW1 I currently use Aquarea Smart Cloud Integration.
But I was always also interested in heishamon, since it provides much more functionalities. Heishamon is running on a ESP8266 and the downside was that it can not run in parallel with the CZ-TAW1. But recently I found a fork from heishamon where the smart user salakrzy showed that it is in fact possible to run both.

Now I thought if this is possible why not try the whole thing with ESPHome?
And in fact it is/will be possible :smiley:

My setup requires the following parts:

  • ESPHome compatible board (I used a ESP32-C3-mini)
  • Bi-Directional Logic Level Converter (UART signal from the heatpump is 5V and not 3.3V)
  • Cable to the Heatpump/CZ-TAW1 (CN-CNT)

For me the cable was the hardest part :smiley: because Panasonic uses a very strange connecter. But fortunatelly panasonic ships their heatpump with a little extension cable which I could use for my purpose :scissors:.



But also if you do not have this cable you may cut your existing cable :scream: or buy the needed parts (see " Where to get connectors" on heishamon github page).

So and here is the wiring:

As you can see the RX and TX lines form the heatpump to CZ-TAW1 are actually not connected. The UART messages will be routed through the ESP32-C3-mini.

And what this image is not showing is that you must also connect your ESP32-C3-mini via USB-Cable to power it. It could maybe also work if you get the 5V from the heatpump but I don’t know if there will be enough current (mA) to drive it.

So first I wanted to see if my wiring actually works and therefore I wrote a ESPHome config to only passthrough and monitor the UART messages.
So this is my ESPHome code:

substitutions:
  mdns_name: panasonic-heatpump
  device_name: Panasonic Heatpump

  pin_rx_hp: GPIO4  # heatpump reads data (RX) on this pin     (yellow)
  pin_tx_hp: GPIO3  # heatpump sends data (TX) on this pin     (green)
  pin_tx_cz: GPIO1  # WiFi module sends data (TX) on this pin  (white)
  pin_rx_cz: GPIO0  # WiFi module reads data (RX) on this pin  (blue)

  # CN-CNT Pinout (from top to bottom)
  # 1 - +5V (250mA)
  # 2 - 0-5V TX (from heatpump)
  # 3 - 0-5V RX (to heatpump)
  # 4 - +12V (250mA)
  # 5 - GND

esp32:
  board: esp32-c3-devkitm-1
  variant: ESP32C3
  framework:
    type: arduino

esphome:
  name: $mdns_name
  friendly_name: $device_name
  comment: $device_description

ota:
  platform: esphome
  password: !secret ota_password
  id: ota_pass

api:
  encryption:
    key: !secret ha_api_key

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "${device_name} Fallback"
    password: !secret fallback_password

web_server:
  port: 80
  auth:
    username: !secret webserver_username
    password: !secret webserver_password

captive_portal:

improv_serial:

logger:

uart:
  - id: uart_hp
    # switch rx and tx to read what the heatpump is sending
    tx_pin: $pin_rx_hp
    rx_pin: $pin_tx_hp
    baud_rate: 9600
    data_bits: 8
    parity: EVEN
    stop_bits: 1
    debug:
      direction: RX
      dummy_receiver: true # needed if no uart component is available
      after:
        bytes: 203
      sequence:
        - lambda: |-
            ESP_LOGD("custom", "Heatpump:");
            UARTDebug::log_hex(direction, bytes, ',');
        - uart.write: # Write rx to tx.
            id: uart_cz
            data: !lambda return bytes ;
  - id: uart_cz
    # switch rx and tx to read what the WiFi module is sending
    tx_pin: $pin_rx_cz
    rx_pin: $pin_tx_cz
    baud_rate: 9600
    data_bits: 8
    parity: EVEN
    stop_bits: 1
    debug:
      direction: RX
      dummy_receiver: true # needed if no uart component is available
      after:
        bytes: 203
      sequence:
        - lambda: |-
            ESP_LOGD("custom", "CZ-TAW1:");
            UARTDebug::log_hex(direction, bytes, ',');
        - uart.write: # Write rx to tx.
            id: uart_hp
            data: !lambda return bytes;

And this is the log output:

So the next part will be to integrate the decode.h/cpp and commands.h/cpp from heishamon :grimacing:

1 Like

Nice work! You must already know that Heishamon now supports relaying the CZ-TAW1 also in the new model with ESP32-S3. But I support you to make a ESPhome version. If you want I can also supply you with a free heishamon with ESP32-S3 and the cables you might still need. Just contact me in a private message.

Thank you for your kind offer @TheHogNL :slight_smile:
Maybe I will take it if I get stuck during my journey :grin:.

Oh interesting, I didn’t know there is a new version of your PCB.
I’m a big fan of your project, so thank you for your hard work.

My personal goal was to find a solution where I don’t need to buy a custom PCB, but where I can use parts that are easy to get (e.g. from AliExpress) and not that expensive. And maybe because I like to solder :sweat_smile:
Also the idea of ESPHome is quite nice, because it makes it easy to customize it to your needs, like adding sensors or Ethernet.

So currently my handmade PCB is still in progress but if its completed I will share it here including my modified extension cable.

Hi EiVit
I am working on a project with ESP32 Wroom to CN-CNT, and are not able to make it reply, is i RS232C or TTL 5V ?
Thanks :slight_smile:

Hi Kristian,

I think it’s TTL because it’s using 0 - 5V.
RS232 can go up to 13V right?
But to be honest I’m not sure what the exact differences are between these protocols, so maybe I’m wrong.
But if you use the code and wiring above it should work.
Sorry the pins in the code were swapped.
I have edited the code above, so now it should be correct.

Hi EIvit
That is the problem, RS232 (C) is ±5V and TTL is 0 / +5V… I must try somthing different.
Have you managed to get the WIFI and ESP to connect at the same time, both as active not (slaves) ?

It’s actually -3V - -15V fol logical 1 and +3V - +15V for logical 0.
So voltage levels are different and logic is inverted…

Hi Kristian,
So I think it is definetly TTL because for RS232 (even if its only 5V) I would need an RS232 adapter.

I don’t know if I understand your question. What do you mean with “active not (slaves)”?

The ESP32 is connected over Wifi with Home Assistant.
And also the CZ-TAW1 Wifi Module is connected over Wifi and is still working as without ESP32. If I open the AQUAREA Smart Cloud I can see the current values and can control the heatpump.
As you can see in the picture in my first post, the ESP32 is working as a man-in-the-middle. If you open the ESPHome Logs of the ESP32 you can see all data the Heatpump is sending and also the data the CZ-TAW1 is sending.

I’m following this thread with excitement. Were you able to make any progress on this part?

Yes indeed I am still working on this project, unfortunately not as fast as I would like.
So first of all I have implemented some sensors to show the values from the heatpump.
For now I have implemented only the first 20 heishamon topics.
But it should not be that difficult to implement the remaining topics.

Here is my esphome yaml file:

substitutions:
  mdns_name: panasonic-heatpump
  device_name: Panasonic Heatpump

  #pin_status_led: GPIO8
  pin_rx_hp: GPIO4  # heatpump reads data (RX) on this pin     (green)
  pin_tx_hp: GPIO3  # heatpump sends data (TX) on this pin     (yellow)
  pin_tx_cz: GPIO1  # WiFi module sends data (TX) on this pin  (blue)
  pin_rx_cz: GPIO0  # WiFi module reads data (RX) on this pin  (white)

  # CN-CNT Pinout (from top to bottom)
  # 1 - +5V (250mA)
  # 2 - 0-5V TX (from heatpump)
  # 3 - 0-5V RX (to heatpump)
  # 4 - +12V (250mA)
  # 5 - GND

esp32:
  board: esp32-c3-devkitm-1
  variant: ESP32C3
  framework:
    type: arduino

esphome:
  name: $mdns_name
  friendly_name: $device_name
  includes:
    - custom_components/heishamon/decode.h
    - custom_components/heishamon/decode.cpp

ota:
  platform: esphome
  password: !secret ota_password
  id: ota_pass

api:
  encryption:
    key: !secret ha_api_key

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "${device_name} Fallback"
    password: !secret fallback_password

web_server:
  port: 80
  auth:
    username: !secret webserver_username
    password: !secret webserver_password

captive_portal:

improv_serial:

logger:

uart:
  - id: uart_hp
    # switch rx and tx to read what the heatpump is sending
    tx_pin: $pin_rx_hp
    rx_pin: $pin_tx_hp
    baud_rate: 9600
    data_bits: 8
    parity: EVEN
    stop_bits: 1
    debug:
      direction: RX
      dummy_receiver: true # needed if no uart component is available
      after:
        bytes: 203
      sequence:
        - lambda: |-
            ESP_LOGD("custom", "Heatpump:");
            UARTDebug::log_hex(direction, bytes, ',');
            if (bytes.size() == 203)
            {
              id(top0).publish_state(toBool(getBit7and8(bytes[4])));
              id(top1).publish_state(getPumpFlow(bytes[169], bytes[170]));
              id(top2).publish_state(toBool(getBit1and2(bytes[4])));
              id(top3).publish_state(toBool(getBit1and2(bytes[7])));
              id(top4).publish_state(getOpMode(bytes[6]));
              id(top5).publish_state(getIntMinus128(bytes[143]) + getFractional(bytes[118], 0));
              id(top6).publish_state(getIntMinus128(bytes[144]) + getFractional(bytes[118], 3));
              id(top7).publish_state(getIntMinus128(bytes[153]));
              id(top8).publish_state(getIntMinus1(bytes[166]));
              id(top9).publish_state(getIntMinus128(bytes[42]));
              id(top10).publish_state(getIntMinus128(bytes[141]));
              id(top11).publish_state(getWord(bytes[182], bytes[183]));
              id(top12).publish_state(getWord(bytes[179], bytes[180]));
              id(top13).publish_state(toBool(getBit1and2(bytes[5])));
              id(top14).publish_state(getIntMinus128(bytes[142]));
              id(top15).publish_state(getPower(bytes[194]));
              id(top16).publish_state(getPower(bytes[193]));
              id(top17).publish_state(getPowerfulmode(getRight3bits(bytes[7])));
              id(top18).publish_state(getQuietmode(getBit3and4and5(bytes[7])));
              id(top19).publish_state(getHolidayState(getBit3and4(bytes[5])));
              id(top20).publish_state(getValve(getBit7and8(bytes[111])));

              id(top92).publish_state(getModel(bytes, 129));
            }
        - uart.write: # Write rx to tx.
            id: uart_cz
            data: !lambda return bytes;
  - id: uart_cz
    # switch rx and tx to read what the WiFi module is sending
    tx_pin: $pin_rx_cz
    rx_pin: $pin_tx_cz
    baud_rate: 9600
    data_bits: 8
    parity: EVEN
    stop_bits: 1
    debug:
      direction: RX
      dummy_receiver: true # needed if no uart component is available
      after:
        bytes: 203
      sequence:
        - lambda: |-
            ESP_LOGD("custom", "CZ-TAW1:");
            UARTDebug::log_hex(direction, bytes, ',');
        - uart.write: # Write rx to tx.
            id: uart_hp
            data: !lambda return bytes;

# # If no CZ-TAW1 available write command to get new data every 5 seconds
# interval:
#   - interval: 5s
#     then:
#       - uart.write:
#           id: uart_hp
#           data: [0x71, 0x6C, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12]

sensor:
  - platform: wifi_signal
    name: WiFi Signal

  - platform: template
    id: top1
    name: "Pump_Flow"
    unit_of_measurement: l/min
    lambda: "return {};"
  - platform: template
    id: top5
    name: "Main_Inlet_Temp"
    unit_of_measurement: °C
    lambda: "return {};"
  - platform: template
    id: top6
    name: "Main_Outlet_Temp"
    unit_of_measurement: °C
    lambda: "return {};"
  - platform: template
    id: top7
    name: "Main_Target_Temp"
    unit_of_measurement: °C
    lambda: "return {};"
  - platform: template
    id: top8
    name: "Compressor_Freq"
    unit_of_measurement: Hz
    lambda: "return {};"
  - platform: template
    id: top9
    name: "DHW_Target_Temp"
    unit_of_measurement: °C
    lambda: "return {};"
  - platform: template
    id: top10
    name: "DHW_Temp"
    unit_of_measurement: °C
    lambda: "return {};"
  - platform: template
    id: top11
    name: "Operations_Hours"
    unit_of_measurement: h
    lambda: "return {};"
  - platform: template
    id: top12
    name: "Operations_Counter"
    lambda: "return {};"
  - platform: template
    id: top14
    name: "Outside_Temp"
    unit_of_measurement: °C
    lambda: "return {};"
  - platform: template
    id: top15
    name: "Heat_Power_Production"
    unit_of_measurement: W
    lambda: "return {};"
  - platform: template
    id: top16
    name: "Heat_Power_Consumption"
    unit_of_measurement: W
    lambda: "return {};"

text_sensor:
  - platform: version
    hide_timestamp: true
    name: ESPHome Version
  - platform: wifi_info
    ip_address:
      name: IP Addresse
      icon: mdi:ip

  - platform: template
    id: top4
    name: "Operating_Mode_State"
    lambda: "return {};"
  - platform: template
    id: top17
    name: "Powerful_Mode_Time"
    lambda: "return {};"
  - platform: template
    id: top18
    name: "Quiet_Mode_Level"
    lambda: "return {};"
  - platform: template
    id: top19
    name: "Holiday_Mode_State"
    lambda: "return {};"
  - platform: template
    id: top20
    name: "ThreeWay_Valve_State"
    lambda: "return {};"

  - platform: template
    id: top92
    name: "Heat_Pump_Model"
    lambda: "return {};"

binary_sensor:
  - platform: status
    name: Status

  - platform: template
    id: top0
    name: "Heatpump_State"
    lambda: "return {};"
  - platform: template
    id: top2
    name: "Force_DHW_State"
    lambda: "return {};"
  - platform: template
    id: top3
    name: "Quiet_Mode_Schedule"
    lambda: "return {};"
  - platform: template
    id: top13
    name: "Main_Schedule_State"
    lambda: "return {};"

button:
  - platform: restart
    name: Neustart

The decode.h file:

#pragma once
#include <cstdint>
#include <cstdlib>
#include <string>
#include <cstring>
#include <vector>

#define NUMBER_OF_MODELS 51
#define NUMBER_OF_TOPICS 127
#define NUMBER_OF_TOPICS_EXTRA 6
#define NUMBER_OF_OPT_TOPICS 7
#define MAX_TOPIC_LEN 42

bool toBool(uint8_t input);

int getBit1(uint8_t input);
int getBit1and2(uint8_t input);
int getBit3and4(uint8_t input);
int getBit5and6(uint8_t input);
int getBit7and8(uint8_t input);
int getBit3and4and5(uint8_t input);
int getLeft5bits(uint8_t input);
int getRight3bits(uint8_t input);
int getIntMinus1(uint8_t input);
int getIntMinus128(uint8_t input);
float getIntMinus1Div5(uint8_t input);
float getIntMinus1Div50(uint8_t input);
int getIntMinus1Times10(uint8_t input);
int getIntMinus1Times50(uint8_t input);
int getPower(uint8_t input);
int getFirstByte(uint8_t input);
int getSecondByte(uint8_t input);
int getWord(uint8_t hi, uint8_t low);
int getUintt16(uint8_t input1, uint8_t input2);
float getPumpFlow(uint8_t input1, uint8_t input2);
float getFractional(uint8_t input, uint8_t shift);

std::string getOpMode(int input);
std::string getPowerfulmode(int input);
std::string getQuietmode(int input);
std::string getHolidayState(int input);
std::string getValve(int input);
std::string getErrorInfo(uint8_t errorType, uint8_t errorNumber);
std::string getBlockedFree(int input);
std::string getHeatCoolModeDesc(int input);
std::string getModel(std::vector<uint8_t>& data, uint8_t index);
std::string getZonesState(int input);
std::string getSolarModeDesc(int input);
std::string getPumpFlowRateMode(int input);
std::string getLiquidType(int input);
std::string getZonesSensorType(int input);
std::string getExtPadHeaterType(int input);
std::string getBivalent(int input);
std::string getMixingValveState(int input);

static const char *ModelNames[] =
{
  "WH-MDC05H3E5", //0
  "WH-MDC07H3E5", //1
  "IDU:WH-SXC09H3E5, ODU:WH-UX09HE5", //2
  "IDU:WH-SDC09H3E8, ODU:WH-UD09HE8", //3
  "IDU:WH-SXC09H3E8, ODU:WH-UX09HE8", //4
  "IDU:WH-SXC12H9E8, ODU:WH-UX12HE8", //5
  "IDU:WH-SXC16H9E8, ODU:WH-UX16HE8", //6
  "IDU:WH-SDC05H3E5, ODU:WH-UD05HE5", //7
  "IDU:WH-SDC0709J3E5, ODU:WH-UD09JE5",  //8
  "WH-MDC05J3E5", //9
  "WH-MDC09H3E5", //10
  "WH-MXC09H3E5", //11
  "IDU:WH-ADC0309J3E5, ODU:WH-UD09JE5", //12
  "IDU:WH-ADC0916H9E8, ODU:WH-UX12HE8", //13
  "IDU:WH-SQC09H3E8, ODU:WH-UQ09HE8", //14
  "IDU:WH-SDC09H3E5, ODU:WH-UD09HE5", //15
  "IDU:WH-ADC0309H3E5, ODU:WH-UD09HE5", //16
  "IDU:WH-ADC0309J3E5, ODU:WH-UD05JE5", //17
  "IDU:WH-SDC0709J3E5, ODU:WH-UD07JE5", //18
  "IDU:WH-SDC07H3E5-1, ODU:WH-UD07HE5-1", //19
  "WH-MDC07J3E5", //20
  "WH-MDC09J3E5", //21
  "IDU:WH-SDC0305J3E5, ODU:WH-UD05JE5", //22
  "WH-MXC09J3E8", //23
  "WH-MXC12J9E8", //24
  "IDU:WH-ADC1216H6E5, ODU:WH-UD12HE5", //25
  "IDU:WH-ADC0309J3E5C, ODU:WH-UD07JE5", //26
  "WH-MDC07J3E5", //27
  "WH-MDC05J3E5", //28
  "IDU:WH-UQ12HE8, ODU:WH-SQC12H9E8", //29
  "IDU:WH-SXC12H6E5, ODU:WH-UX12HE5", //30
  "WH-MDC09J3E5", //31
  "WH-MXC09J3E5", //32
  "IDU:WH-ADC1216H6E5C ODU:WH-UD12HE5", //33
  "IDU:WH-ADC0509L3E5 ODU:WH-WDG07LE5", //34
  "IDU:WH-SXC09H3E8 ODU:WH-UX09HE8", //35
  "IDU:WH-ADC0309K3E5AN ODU:WH-UDZ07KE5", //36
  "IDU:WH-SDC0309K3E5 ODU:WH-UDZ05KE5", //37
  "IDU:WH-SDC0509L3E5 ODU:WH-WDG09LE5", //38
  "IDU:WH-SDC12H9E8 ODU:WH-UD12HE8", //39
  "IDU:WH-SDC0309K3E5, ODU:WH-UDZ07KE5", //40
  "IDU:WH-ADC0916H9E8, ODU:WH-UX16HE8", //41
  "IDU:WH-ADC0912H9E8, ODU:WH-UX12HE8", //42
  "WH-MXC16J9E8", //43
  "WH-MXC12J6E5", //44
  "IDU:WH-SQC09H3E8, ODU:WH-UQ09HE8", //45
  "IDU:WH-ADC0309K3E5 ODU:WH-UDZ09KE5", //46
  "IDU:WH-ADC0916H9E8, ODU:WH-UX12HE8", //47
  "IDU:WH-SDC0509L3E5 ODU:WH-WDG07LE5", //48
  "IDU:WH-SXC09H3E5, ODU:WH-UX09HE5", //49
  "IDU:WH-SXC12H9E8, ODU:WH-UX12HE8", //50
};

//stores the bytes #129 to #138 of known models in the same order as the const above
static const uint8_t KnownModels[NUMBER_OF_MODELS][10] =
{
  0xE2, 0xCF, 0x0B, 0x13, 0x33, 0x32, 0xD1, 0x0C, 0x16, 0x33, //0
  0xE2, 0xCF, 0x0B, 0x14, 0x33, 0x42, 0xD1, 0x0B, 0x17, 0x33, //1
  0xE2, 0xCF, 0x0D, 0x77, 0x09, 0x12, 0xD0, 0x0B, 0x05, 0x11, //2
  0xE2, 0xCF, 0x0C, 0x88, 0x05, 0x12, 0xD0, 0x0B, 0x97, 0x05, //3
  0xE2, 0xCF, 0x0D, 0x85, 0x05, 0x12, 0xD0, 0x0C, 0x94, 0x05, //4
  0xE2, 0xCF, 0x0D, 0x86, 0x05, 0x12, 0xD0, 0x0C, 0x95, 0x05, //5
  0xE2, 0xCF, 0x0D, 0x87, 0x05, 0x12, 0xD0, 0x0C, 0x96, 0x05, //6
  0xE2, 0xCE, 0x0D, 0x71, 0x81, 0x72, 0xCE, 0x0C, 0x92, 0x81, //7
  0x62, 0xD2, 0x0B, 0x43, 0x54, 0x42, 0xD2, 0x0B, 0x72, 0x66, //8
  0xC2, 0xD3, 0x0B, 0x33, 0x65, 0xB2, 0xD3, 0x0B, 0x94, 0x65, //9
  0xE2, 0xCF, 0x0B, 0x15, 0x33, 0x42, 0xD1, 0x0B, 0x18, 0x33, //10
  0xE2, 0xCF, 0x0B, 0x41, 0x34, 0x82, 0xD1, 0x0B, 0x31, 0x35, //11
  0x62, 0xD2, 0x0B, 0x45, 0x54, 0x42, 0xD2, 0x0B, 0x47, 0x55, //12
  0xE2, 0xCF, 0x0C, 0x74, 0x09, 0x12, 0xD0, 0x0D, 0x95, 0x05, //13
  0xE2, 0xCF, 0x0B, 0x82, 0x05, 0x12, 0xD0, 0x0C, 0x91, 0x05, //14
  0xE2, 0xCF, 0x0C, 0x55, 0x14, 0x12, 0xD0, 0x0B, 0x15, 0x08, //15
  0xE2, 0xCF, 0x0C, 0x43, 0x00, 0x12, 0xD0, 0x0B, 0x15, 0x08, //16
  0x62, 0xD2, 0x0B, 0x45, 0x54, 0x32, 0xD2, 0x0C, 0x45, 0x55, //17
  0x62, 0xD2, 0x0B, 0x43, 0x54, 0x42, 0xD2, 0x0C, 0x46, 0x55, //18
  0xE2, 0xCF, 0x0C, 0x54, 0x14, 0x12, 0xD0, 0x0B, 0x14, 0x08, //19
  0xC2, 0xD3, 0x0B, 0x34, 0x65, 0xB2, 0xD3, 0x0B, 0x95, 0x65, //20
  0xC2, 0xD3, 0x0B, 0x35, 0x65, 0xB2, 0xD3, 0x0B, 0x96, 0x65, //21
  0x62, 0xD2, 0x0B, 0x41, 0x54, 0x32, 0xD2, 0x0C, 0x45, 0x55, //22
  0x32, 0xD4, 0x0B, 0x87, 0x84, 0x73, 0x90, 0x0C, 0x84, 0x84, //23
  0x32, 0xD4, 0x0B, 0x88, 0x84, 0x73, 0x90, 0x0C, 0x85, 0x84, //24
  0xE2, 0xCF, 0x0B, 0x75, 0x09, 0x12, 0xD0, 0x0C, 0x06, 0x11, //25
  0x42, 0xD4, 0x0B, 0x83, 0x71, 0x42, 0xD2, 0x0C, 0x46, 0x55, //26
  0xC2, 0xD3, 0x0C, 0x34, 0x65, 0xB2, 0xD3, 0x0B, 0x95, 0x65, //27
  0xC2, 0xD3, 0x0C, 0x33, 0x65, 0xB2, 0xD3, 0x0B, 0x94, 0x65, //28
  0xE2, 0xCF, 0x0B, 0x83, 0x05, 0x12, 0xD0, 0x0D, 0x92, 0x05, //29
  0xE2, 0xCF, 0x0C, 0x78, 0x09, 0x12, 0xD0, 0x0B, 0x06, 0x11, //30
  0xC2, 0xD3, 0x0C, 0x35, 0x65, 0xB2, 0xD3, 0x0B, 0x96, 0x65, //31
  0x32, 0xD4, 0x0B, 0x99, 0x77, 0x62, 0x90, 0x0B, 0x01, 0x78, //32
  0x42, 0xD4, 0x0B, 0x15, 0x76, 0x12, 0xD0, 0x0B, 0x10, 0x11, //33
  0xE2, 0xD5, 0x0C, 0x29, 0x99, 0x83, 0x92, 0x0C, 0x28, 0x98, //34
  0xE2, 0xCF, 0x0D, 0x85, 0x05, 0x12, 0xD0, 0x0E, 0x94, 0x05, //35
  0xE2, 0xD5, 0x0D, 0x36, 0x99, 0x02, 0xD6, 0x0F, 0x67, 0x95, //36
  0xE2, 0xD5, 0x0B, 0x08, 0x95, 0x02, 0xD6, 0x0E, 0x66, 0x95, //37
  0xE2, 0xD5, 0x0B, 0x34, 0x99, 0x83, 0x92, 0x0C, 0x29, 0x98, //38
  0xE2, 0xCF, 0x0C, 0x89, 0x05, 0x12, 0xD0, 0x0C, 0x98, 0x05, //39
  0xE2, 0xD5, 0x0B, 0x08, 0x95, 0x02, 0xD6, 0x0E, 0x67, 0x95, //40
  0xE2, 0xCF, 0x0C, 0x74, 0x09, 0x12, 0xD0, 0x0C, 0x96, 0x05, //41
  0xE2, 0xCF, 0x0C, 0x74, 0x09, 0x12, 0xD0, 0x0E, 0x95, 0x05, //42
  0x32, 0xD4, 0x0B, 0x89, 0x84, 0x73, 0x90, 0x0C, 0x86, 0x84, //43
  0x32, 0xD4, 0x0B, 0x00, 0x78, 0x62, 0x90, 0x0B, 0x02, 0x78, //44
  0xE2, 0xCF, 0x0B, 0x82, 0x05, 0x12, 0xD0, 0x0D, 0x91, 0x05, //45
  0xE2, 0xD5, 0x0D, 0x99, 0x94, 0x02, 0xD6, 0x0D, 0x68, 0x95, //46
  0xE2, 0xCF, 0x0C, 0x74, 0x09, 0x12, 0xD0, 0x0C, 0x95, 0x05, //47
  0xE2, 0xD5, 0x0B, 0x34, 0x99, 0x83, 0x92, 0x0C, 0x28, 0x98, //48
  0xE2, 0xCF, 0x0D, 0x77, 0x09, 0x12, 0xD0, 0x0C, 0x05, 0x11, //49
  0xE2, 0xCF, 0x0D, 0x86, 0x05, 0x12, 0xD0, 0x0E, 0x95, 0x05, //50
};

The decode.cpp file:

#include "decode.h"

bool toBool(uint8_t input)
{
  return input != 0;
}

int getBit1(uint8_t input)
{
  return (input >> 7);
}

int getBit1and2(uint8_t input)
{
  return ((input >> 6) - 1);
}

int getBit3and4(uint8_t input)
{
  return (((input >> 4) & 0b11) - 1);
}

int getBit5and6(uint8_t input)
{
  return (((input >> 2) & 0b11) - 1);
}

int getBit7and8(uint8_t input)
{
  return ((input & 0b11) - 1);
}

int getBit3and4and5(uint8_t input)
{
  return (((input >> 3) & 0b111) - 1);
}

int getLeft5bits(uint8_t input)
{
  return ((input >> 3) - 1);
}

int getRight3bits(uint8_t input)
{
  return ((input & 0b111) - 1);
}

int getIntMinus1(uint8_t input)
{
  int value = (int)input - 1;
  return value;
}

int getIntMinus128(uint8_t input)
{
  int value = (int)input - 128;
  return value;
}

float getIntMinus1Div5(uint8_t input)
{
  return ((((float)input - 1) / 5), 1);
}

float getIntMinus1Div50(uint8_t input)
{
  return ((((float)input - 1) / 50), 2);
}

int getIntMinus1Times10(uint8_t input)
{
  int value = (int)input - 1;
  return (value * 10);
}

int getIntMinus1Times50(uint8_t input)
{
  int value = (int)input - 1;
  return (value * 50);
}

int getPower(uint8_t input)
{
  int value = ((int)input - 1) * 200;
  return value;
}

int getFirstByte(uint8_t input)
{
  return ((input >> 4) - 1);
}

int getSecondByte(uint8_t input)
{
  return ((input & 0b1111) - 1);
}

int getWord(uint8_t low, uint8_t hi)
{
  return ((hi << 8) + low) - 1;
}

int getUintt16(uint8_t input1, uint8_t input2)
{
  uint16_t value = static_cast<uint16_t>((input2 << 8) | input1);
  return (value - 1);
}

// input1 = data[169]
// input2 = data[170]
float getPumpFlow(uint8_t input1, uint8_t input2)
{ // TOP1 //
  int PumpFlow2 = (int)input2;
  float PumpFlow1 = (((float)input1 - 1) / 256);
  float PumpFlow = PumpFlow2 + PumpFlow1;
  return PumpFlow;
}

float getFractional(uint8_t input, uint8_t shift)
{
  float result;
  int fractional = (int)((input >> shift) & 0b111) - 1;
  result = fractional * 0.25;
  return result;
}



std::string getOpMode(int input)
{
  switch ((input & 0b111111))
  {
  case 18:
    return "Heat";
  case 19:
    return "Cool";
  case 25:
    return "Auto(heat)";
  case 33:
    return "DHW";
  case 34:
    return "Heat+DHW";
  case 35:
    return "Cool+DHW";
  case 41:
    return "Auto(heat)+DHW";
  case 26:
    return "Auto(cool)";
  case 42:
    return "Auto(cool)+DHW";
  default:
    return "UNKNOWN";
  }
}

std::string getPowerfulmode(int input)
{
  switch (input)
  {
  case 0:
    return "Off";
  case 1:
    return "30min";
  case 2:
    return "60min";
  case 3:
    return "90min";
  default:
    return "UNKNOWN";
  }
}

std::string getQuietmode(int input)
{
  switch (input)
  {
  case 0:
    return "Off";
  case 1:
    return "Level 1";
  case 2:
    return "Level 2";
  case 3:
    return "Level 3";
  default:
    return "UNKNOWN";
  }
}

std::string getHolidayState(int input)
{
  switch (input)
  {
  case 0:
    return "Off";
  case 1:
    return "Scheduled";
  case 2:
    return "Active";
  default:
    return "UNKNOWN";
  }
}

std::string getValve(int input)
{
  switch (input)
  {
  case 0:
    return "Room";
  case 1:
    return "DHW";
  default:
    return "UNKNOWN";
  }
}

// errorType = data[113]
// errorNumber = data[114]
std::string getErrorInfo(uint8_t errorType, uint8_t errorNumber)
{ // TOP44 //
  int error_type = (int)(errorType);
  int error_number = ((int)(errorNumber)) - 17;
  char error_string[10];

  switch (error_type)
  {
  case 177: // B1=F type error
    sprintf(error_string, "F%02X", error_number);
    break;
  case 161: // A1=H type error
    sprintf(error_string, "H%02X", error_number);
    break;
  default:
    sprintf(error_string, "No error");
    break;
  }

  return error_string;
}

std::string getBlockedFree(int input)
{
  switch (input)
  {
  case 0:
    return "Blocked";
  case 1:
    return "Free";
  default:
    return "UNKNOWN";
  }
}

std::string getHeatCoolModeDesc(int input)
{
  switch (input)
  {
  case 0:
    return "Comp. Curve";
  case 1:
    return "Directe";
  default:
    return "UNKNOWN";
  }
}

std::string getModel(std::vector<uint8_t>& data, uint8_t index)
{ // TOP92 //
  uint8_t model[10] = { data[index], data[index+1], data[index+2], data[index+3], data[index+4], data[index+5], data[index+6], data[index+7], data[index+8], data[index+9] };
  int result = -1;
  std::string returnValue = "UNKNOWN";

  for (unsigned int i = 0 ; i < sizeof(KnownModels) / sizeof(KnownModels[0]) ; i++)
  {
    if (memcmp(model, KnownModels[i], 10) == 0)
    {
      result = i;
    }
  }

  if (result >= 0 && result < sizeof(ModelNames) / sizeof(ModelNames[0]))
  {
    returnValue = ModelNames[result];
  }

  return returnValue;
}

std::string getZonesState(int input)
{
  switch (input)
  {
  case 0:
    return "Zone1 active";
  case 1:
    return "Zone2 active";
  case 2:
    return "Zone1 and zone2 active";
  default:
    return "UNKNOWN";
  }
}

std::string getSolarModeDesc(int input)
{
  switch (input)
  {
  case 0:
    return "Disabled";
  case 1:
    return "Buffer";
  case 2:
    return "DHW";
  default:
    return "UNKNOWN";
  }
}

std::string getPumpFlowRateMode(int input)
{
  switch (input)
  {
  case 0:
    return "DeltaT";
  case 1:
    return "Max flow";
  default:
    return "UNKNOWN";
  }
}

std::string getLiquidType(int input)
{
  switch (input)
  {
  case 0:
    return "Water";
  case 1:
    return "Glycol";
  default:
    return "UNKNOWN";
  }
}

std::string getZonesSensorType(int input)
{
  switch (input)
  {
  case 0:
    return "Water Temperature";
  case 1:
    return "External Thermostat";
  case 2:
    return "Internal Thermostat";
  case 3:
    return "Thermistor";
  default:
    return "UNKNOWN";
  }
}

std::string getExtPadHeaterType(int input)
{
  switch (input)
  {
  case 0:
    return "Disabled";
  case 1:
    return "Type-A";
  case 2:
    return "Type-B";
  default:
    return "UNKNOWN";
  }
}

std::string getBivalent(int input)
{
  switch (input)
  {
  case 0:
    return "Off";
  case 1:
    return "Alternativ";
  case 2:
    return "A-Off";
  case 3:
    return "Parallel";
  case 4:
    return "P-Off";
  case 5:
    return "Parallel Advanced";
  default:
    return "UNKNOWN";
  }
}

std::string getMixingValveState(int input)
{
  switch (input)
  {
  case 0:
    return "Off";
  case 1:
    return "Increase";
  case 2:
    return "Nothing";
  case 3:
    return "Decrease";
  default:
    return "UNKNOWN";
  }
}

The files must be located as follows:

esphome
├── panasonic_heatpump.yaml
└── custom_components
    └── heishamon
        ├── decode.h
        └── decode.cpp

For those who are interested in this project I have uploaded my code to GitHub.

Here the link:

I have created also an excel file where you can copy & paste the code for each topic to create a sensor in your yaml file.

If someone has a working Heishamon device it would be nice if my code could be verified against the Heishamon code.

During the past days I had some time to work on this project.
So I made an external component :tada:

Now the code look much cleaner.
Currently it is only possible to read values send from the heatpump.
But I plan also to send commands to the heatpump.
Also currently the seond UART (CZ-TAW1) is mandatory because it sends the requests to the heatpump.
My goal is also to make this optional so the esp32 will send the commands.

Please expect breaking changes on this component because it is still under development.

Here the link to an example yaml file:

Hi all,
here again an update on my project.
As promised I have added some features to the panasonic heatpump esphome component.

Now it is possible to run the ESP controller without the CZ-TAW1 module. That means the ESP controller will send the polling requests each 3 seconds. But the polling interval is also configurable. To run the ESP controller in standalone mode no changes on the wiring are needed (but if you want you can also remove simply the connector + cables to the CZ-TAW1).

It is also possible to send commands to the heatpump now. This works with CZ-TAW1 (man-in-the-middle-mode) and also without (standalone-mode).
The decoding calculation for all sensors and controls was taken from ProtocolByteDecrypt.md from the Heishamon project. To keep the reference all sensors and controls are named as the TOP and SET topics from Heishamon.

Please see the README.md file for more informations.

And for those who are interested how my ESP board looks in real life here a picture:

I have prepared the same device with ESP32 C3 Supermini, the necessary connectors, etc. Connected to the heatpump, but something is wrong, all sensors have unknown values and I receive the following messages in the webserver:

Time level Tag Message
11:27:06 [I] [panasonic_heatpump:207] >>> polling_request[111]
11:27:06 [I] [panasonic_heatpump:220] >>> 71,6C,01,10,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:27:06 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:27:06 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,12
11:27:06 [W] [component:237] Component panasonic_heatpump took a long time for an operation (170 ms).
11:27:06 [W] [component:238] Components should block for at most 30 ms.
11:27:06 [I] [safe_mode:041] Boot seems successful; resetting boot loop counter
11:27:06 [D] [esp32.preferences:114] Saving 1 preferences to flash…
11:27:06 [D] [esp32.preferences:143] Saving 1 preferences to flash: 0 cached, 1 written, 0 failed
11:27:35 [I] [panasonic_heatpump:207] >>> polling_request[111]
11:27:36 [I] [panasonic_heatpump:220] >>> 71,6C,01,10,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:27:36 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:27:36 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,12
11:27:36 [W] [component:237] Component panasonic_heatpump took a long time for an operation (170 ms).
11:27:36 [W] [component:238] Components should block for at most 30 ms.
11:28:05 [I] [panasonic_heatpump:207] >>> polling_request[111]
11:28:06 [I] [panasonic_heatpump:220] >>> 71,6C,01,10,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:28:06 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:28:06 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,12
11:28:06 [W] [component:237] Component panasonic_heatpump took a long time for an operation (171 ms).
11:28:06 [W] [component:238] Components should block for at most 30 ms.
11:28:35 [I] [panasonic_heatpump:207] >>> polling_request[111]
11:28:36 [I] [panasonic_heatpump:220] >>> 71,6C,01,10,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:28:36 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:28:36 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,12
11:28:36 [W] [component:237] Component panasonic_heatpump took a long time for an operation (170 ms).
11:28:36 [W] [component:238] Components should block for at most 30 ms.
11:29:05 [I] [panasonic_heatpump:207] >>> polling_request[111]
11:29:06 [I] [panasonic_heatpump:220] >>> 71,6C,01,10,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:29:06 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:29:06 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,12
11:29:06 [W] [component:237] Component panasonic_heatpump took a long time for an operation (170 ms).
11:29:06 [W] [component:238] Components should block for at most 30 ms.
11:29:35 [I] [panasonic_heatpump:207] >>> polling_request[111]
11:29:35 [I] [panasonic_heatpump:220] >>> 71,6C,01,10,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:29:36 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,
11:29:36 [I] [panasonic_heatpump:220] >>> 00,00,00,00,00,00,00,00,12
11:29:36 [W] [component:237] Component panasonic_heatpump took a long time for an operation (170 ms).
11:29:36 [W] [component:238] Components should block for at most 30 ms.

What can be problem?

Used the yaml from here: esphome_components/components/panasonic_heatpump/README.md at 0bed3d930a1515c4a91fbc14a98cfc3845c7e322 · ElVit/esphome_components · GitHub with a modification that the poll interval is 30s.

Hi @ethgkk,

The log says that your ESP controller is sending requests but it gets no response from the heatpump.
I had the same issue so I wrote it down in the README.md file.

You must simply switch off the power source of the heatpump and then switch it on again. This fixed usually the issue for me. Of course it could also be a wiring issue, so please check also your wiring.

Hi ElVit,

This is not the case unfortunately. I inserted when the heatpump was powered off. However today I found during trouble shooting that the logic converter is dead. It has the right voltages everywhere, but the 5V pins are always 0. Checked with a simple ESP32 code where i defined the pins as switches. On the 3.3V side, it gets the 3.3V when I turn on the switch, however on the 5V side nothing happens. Checked another logic converter (I bought two), with the same result. Most probably both were damaged during the shipping. Lessons learnt, all components must be checked in a standalone mode before soldering everything together…

I’m trying to start communication on my TCAP pump. After loading the program into ESP, I don’t see any movement in the logs. Movement only appears when I press a button, e.g. turn on the pump. Apart from that, I don’t see any movement, neither from the pump nor from CZ-TAW1. Parameter log_uart_msg: true. Hardware and configuration as above. What could be the reason?

I managed to start the communication, now it works. The colors in my pump are different so I had to swap the pins. I have the colors in the following order: 1 red; 2 green; 3 white; 4 free; 5 black. Additionally, I gave up my name and restored the name from your example. Now everything works properly in the Panasonic cloud, but I noticed a few problems with esphome and home assistant.

  1. I don’t know why but the control from Home Assistant does not work properly. The entity states differ in relation to the actual state of the pump. Through the web server everything is ok. For example: the pump is in “heating only” mode and in HA I have “DHW only” mode, through the web server the correct mode is displayed as “Heat only”. Second example: I turn off the pump from HA using the switch, the pump physically turns off but in the system I still have the state that it is on. In the panasonic cloud the pump status is correct, i.e. the pump is off
  2. When controlling the output temperature directly and not according to the curve, you cannot set the temperature because the range is -5 to 5 and should be 20 to 60, so I cannot set, for example, 38 degrees Celsius.

hellow, what hardware does you use?