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:

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.