Helios EasyControl 3.0 to HomeAssistant custom integration

After I got my KWL360, I couldn’t find anything to bring my KWL into home assistant like I expected and wanted it.

But I found the groundwork of sanchosk.

This I transferred to python and extended it. I also put it into a custom integration which could be installed via hacs (at least via custom repo for the moment :wink:)

The integration can read all the sensors that I find useful at the moment:

grafik

It also can change the KWL mode via a select:
grafik

And finally it also can change the time and fan speed for the intensive mode:
grafik

The integration should work with the new Helios EasyControl 3.0 controller, which communicates via WebSocket, not like the old ones via ModBus over TCP.

The logic is calling the websocket every 60s to read the current data and uses the serial number of the device to generate unique ids.

The sources are here: github

If anyone has a picture we could use for the integration, I would be happy to add it.

If there are any additional wishes or problems :fearful: let me know, and I will see what I can do.

Great to see this! I was just thinking about finally integrating my helios. I will try this as soon as I get home!

Thanks for the work!

As discussed, what information do you need to integrate the original Helios Co2 sensor?
Thanks for the great work.

Works like a charm! Thanks!

1 Like

are you still working on this?
I am working on the same for openhab and did some further deconding of the messages, which might help.

The messages to set variables are all structured the same:
split them into words (2byte) in little endian.

example message (everything in hex):
0400 f900 1550 3d00 4f51

0400 → 0004 → 4 words long message
f900 → 00f9 → no idea. same for all messages that set variables
1550 → 5015 → id of the variable to set (this e.g. is the fan speed for away)
3d00 → 003d → 61 decimal → value of the variable
4f51 → 514f → checksum

The checksum is basically the sum of all previous words. For the example above:
0004+00f9+5015+003d = 514f

The example above sets the fan speed for away to 61%.

Below all variables i figured out so far (already in little endian):

rel. hum. away 1350 (0 or 1)
fan away 1550 (0-100)
temp away 1650 (strange temp. value)

rel. hum. home 1950
fan home 1b50
temp home 1c50

rel. hum. intense 1f50
fan intense 2150
temp intense 2250
time intense 4050 (in minutes)
timer (on/off) intense 0655

Fan in individ. 0850
Fan out individ. 0750
temp individ. 1150
time individ. 5041
timer (on/off) individ. 0755

bypass on/off 4850

filterinterval 3950 (in days)
filter exchange date 4250/4350/4450 (Y/M/D)
0800 f900 4250 1900 4350 0400 4450 1900 00f2
→ 8 words long. 3 times variable + value

Setting the mode is a bit strange
0112 set to 1 is away, set to 0 is home. Further the timers for individual (0512) and intensive (0412) are set to 0:
e.g.
0800 f900 0112 0100 0412 0000 0512 0000 0c37

To set individual or intensive, the timers are set to the respective value (the other one is set to 0):
Timer Int: 0412 (in minutes)
Timer Ind: 0512 (in minutes)
0600 f900 0412 3c00 0512 0000 4425
→ this one sets Intensive (0412) to 3c00 → 003c → 60 minutes and individual (0512) to 0 minutes

Yes, I will continue to work on it once I find more time :smiley:

Thank you for your input, I will use it to extend the other properties.

Hi everyone,
Based on this integration, I created a Lovelace UI. It’s my first attempt, so it’s not perfect — but maybe it helps someone as a starting point.
Feel free to improve it or build on it!

Cheers,
Timon

type: custom:mod-card
style: |
  ha-card {
    border-radius: 14px;
   box-shadow: 0 4px 24px 0 #0004;
    padding: 10px;
    border: none;
    background: #1D1D1D;
  }
card:
  type: vertical-stack
  cards:
    - type: custom:button-card
      entity: switch.kwl_on_off_switch
      icon: mdi:fan
      name: Belüftungsanlage
      label: Air Conditioner
      show_icon: true
      tap_action:
        action: toggle
      state:
        - value: "on"
          spin: true
          styles:
            card:
              - background: none
            icon:
              - animation: rotating 5s linear infinite
        - value: "off"
          icon: mdi:fan-off
          styles:
            card:
              - background: none
      custom_fields:
        kwlstate: |
          [[[
            const mode = states['select.select_state_of_the_kwl']?.state || 'Unknown';
            const iconMap = {
              AtHome:      {icon: "mdi:home-variant",      color:"#2d9cfa",  label:"Home"},
              Away:        {icon: "mdi:account-arrow-right",color:"#607d8b", label:"Away"},
              Intensive:   {icon: "mdi:flash",             color:"#fea726",  label:"Boost"},
              Individual:  {icon: "mdi:autorenew",         color:"#7c4dff",  label:"Auto"},
            };
            const meta = iconMap[mode] || {icon:"mdi:help-circle", color:"#757575", label:mode};
            return `
              <span style="
                display:inline-flex;align-items:center;
                gap:9px;background:${meta.color};color:white;
                border-radius:14px;
                padding:3px 16px 3px 12px;
                font-size:15px;font-weight:600;box-shadow:0 1px 4px 0 rgba(0,0,0,0.10);letter-spacing:0.5px;">
                <ha-icon icon="${meta.icon}" style="width:19px;height:19px;margin-right:2px;color:white;opacity:0.87"></ha-icon>
                ${meta.label}
              </span>
            `;
          ]]]
        fanspeed: |
          [[[
            const percent = parseInt(states['sensor.kwl_220_d_l_current_fan_speed']?.state || 0, 10);
            const is_on = states['switch.kwl_on_off_switch']?.state === "on";
            const fan_icon = is_on ? "mdi:fan" : "mdi:fan-off";
            const size = 80;
            const stroke = 4;
            const radius = (size - stroke) / 2;
            const circumference = 2 * Math.PI * radius;
            const offset = circumference * (1 - percent / 100);
            const innerPadding = 10;

            return `
              <style>
                @keyframes spin-fan {
                  0% { transform: rotate(0deg);}
                  100% { transform: rotate(360deg);}
                }
              </style>
              <div style="position:relative;width:${size}px;height:${size}px;display:flex;align-items:center;justify-content:center;">
                <svg width="${size}" height="${size}" style="position:absolute;top:0;left:0;">
                  <circle
                    cx="${size/2}" cy="${size/2}" r="${radius}"
                    stroke="#122E63"
                    stroke-opacity="0.25"
                    stroke-width="${stroke}"
                    fill="none"
                  />
                  <circle
                    cx="${size/2}" cy="${size/2}" r="${radius}"
                    stroke="#2D9CFA"
                    stroke-width="${stroke}"
                    fill="none"
                    stroke-dasharray="${circumference}"
                    stroke-dashoffset="${offset}"
                    stroke-linecap="round"
                    transform="rotate(-90 ${size/2} ${size/2})"
                  />
                </svg>
                <div style="position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:1;padding:${innerPadding}px;">
                  <ha-icon icon="${fan_icon}" style="color:#2D9CFA;width:22px;height:22px;${is_on ? 'animation: spin-fan 4s linear infinite;' : ''};margin-bottom:2px;"></ha-icon>
                  <span style="font-size:20px;font-weight:600;color:white;margin-top:2px;">${percent}%</span>
                </div>
              </div>
            `;
          ]]]
        icon: ""
        exhaust: |
          [[[
            return '<div style="color: #FF6F61;">Exhaust</div>' + (states['sensor.kwl_220_d_l_exhaust_temperature']?.state || 'N/A') + '°C';
          ]]]
        supply: |
          [[[
            return '<div style="color: #6BB9FF;">Supply</div>' + (states['sensor.kwl_220_d_l_supply_temperature']?.state || 'N/A') + '°C';
          ]]]
      styles:
        card:
          - padding: 0px 24px 12px 24px
          - background: none
        grid:
          - grid-template-areas: |
              ".  name      kwlstate"
              "supply fanspeed exhaust"
          - grid-template-columns: 1fr 1fr 1fr
          - grid-template-rows: min-content min-content min-content min-content min-content
        name:
          - grid-area: name
          - justify-self: start
          - font-size: 16px
          - font-weight: 500
          - color: rgba(255, 255, 255, 0.85)
        custom_fields:
          kwlstate:
            - grid-area: kwlstate
            - justify-self: end
            - align-self: start
            - margin-top: 0px
            - margin-right: 0px
          exhaust:
            - grid-area: exhaust
            - font-size: 20px
            - font-weight: 300
            - color: rgba(255, 255, 255, 0.85)
            - justify-self: end
            - margin-top: 20px
          supply:
            - grid-area: supply
            - font-size: 20px
            - font-weight: 300
            - color: rgba(255, 255, 255, 0.85)
            - justify-self: start
            - margin-top: 20px
          fanspeed:
            - grid-area: fanspeed
            - justify-self: center
            - align-self: center
            - margin-top: 8px
            - margin-bottom: 0px
        img_cell:
          - position: absolute
          - left: 0
          - top: 0px
          - width: 32px
          - height: 32px
          - justify-self: baseline
          - background-color: rgba(55,55,55,1)
          - border-radius: 50%
          - padding: 4px
        icon:
          - grid-area: icon
          - width: 64px
          - color: white
    - type: horizontal-stack
      cards:
        - type: custom:button-card
          entity: select.select_state_of_the_kwl
          name: Home
          icon: mdi:home-variant
          tap_action:
            action: call-service
            service: select.select_option
            data:
              entity_id: select.select_state_of_the_kwl
              option: AtHome
          styles:
            card:
              - border-radius: 12px
              - background-color: |
                  [[[
                    return entity.state === "AtHome" ? "#2d9cfa" : "rgba(255,255,255,0.06)";
                  ]]]
              - color: |
                  [[[
                    return entity.state === "AtHome" ? "#fff" : "#7b8ca7";
                  ]]]
              - font-weight: 600px
              - box-shadow: |
                  [[[
                    return entity.state === "AtHome" ? "0 2px 8px 0 #2d9cfa30" : "none";
                  ]]]
              - min-width: 56px
              - min-height: 46px
              - transition: 0.22s
              - cursor: pointer
            icon:
              - color: |
                  [[[
                    return entity.state === "AtHome" ? "#fff" : "#4b5a70";
                  ]]]
              - width: 26px
              - height: 26px
        - type: custom:button-card
          entity: select.select_state_of_the_kwl
          name: Away
          icon: mdi:account-arrow-right
          tap_action:
            action: call-service
            service: select.select_option
            data:
              entity_id: select.select_state_of_the_kwl
              option: Away
          styles:
            card:
              - border-radius: 12px
              - background-color: |
                  [[[
                    return entity.state === "Away" ? "#607d8b" : "rgba(255,255,255,0.06)";
                  ]]]
              - color: |
                  [[[
                    return entity.state === "Away" ? "#fff" : "#7b8ca7";
                  ]]]
              - font-weight: 600px
              - box-shadow: |
                  [[[
                    return entity.state === "Away" ? "0 2px 8px 0 #607d8b30" : "none";
                  ]]]
              - min-width: 56px
              - min-height: 46px
              - transition: 0.22s
              - cursor: pointer
            icon:
              - color: |
                  [[[
                    return entity.state === "Away" ? "#fff" : "#4b5a70";
                  ]]]
              - width: 26px
              - height: 26px
        - type: custom:button-card
          entity: select.select_state_of_the_kwl
          name: Boost
          icon: mdi:flash
          tap_action:
            action: call-service
            service: select.select_option
            data:
              entity_id: select.select_state_of_the_kwl
              option: Intensive
          styles:
            card:
              - border-radius: 12px
              - background-color: |
                  [[[
                    return entity.state === "Intensive" ? "#fea726" : "rgba(255,255,255,0.06)";
                  ]]]
              - color: |
                  [[[
                    return entity.state === "Intensive" ? "#fff" : "#7b8ca7";
                  ]]]
              - font-weight: 600px
              - box-shadow: |
                  [[[
                    return entity.state === "Intensive" ? "0 2px 8px 0 #fea72630" : "none";
                  ]]]
              - min-width: 56px
              - min-height: 46px
              - transition: 0.22s
              - cursor: pointer
            icon:
              - color: |
                  [[[
                    return entity.state === "Intensive" ? "#fff" : "#4b5a70";
                  ]]]
              - width: 26px
              - height: 26px
        - type: custom:button-card
          entity: select.select_state_of_the_kwl
          name: Auto
          icon: mdi:autorenew
          tap_action:
            action: call-service
            service: select.select_option
            data:
              entity_id: select.select_state_of_the_kwl
              option: Individual
          styles:
            card:
              - border-radius: 12px
              - background-color: |
                  [[[
                    return entity.state === "Individual" ? "#7c4dff" : "rgba(255,255,255,0.06)";
                  ]]]
              - color: |
                  [[[
                    return entity.state === "Individual" ? "#fff" : "#7b8ca7";
                  ]]]
              - font-weight: 600px
              - box-shadow: |
                  [[[
                    return entity.state === "Individual" ? "0 2px 8px 0 #7c4dff30" : "none";
                  ]]]
              - min-width: 56px
              - min-height: 46px
              - transition: 0.22s
              - cursor: pointer
            icon:
              - color: |
                  [[[
                    return entity.state === "Individual" ? "#fff" : "#4b5a70";
                  ]]]
              - width: 26px
              - height: 26px

Looks cool :smiley:

Hi everyone

Oh no, I had it really working and it worked perfectly.
But suddenly it stopped working again. Probably after an update from HA. All entities were inactive so I uninstalled it and tried to install it again.

Now I get the message ‘cannot_connect’ or ‘unknown’ again when I try to install the integration. I had the same problem before, but i can’t remember how I solved it :slight_smile:

If I interpret it correctly, the config script fails at the function: easyControlsInstance.test_connection().
If I replace the response with ‘true’, I can install the integration, but no entities are created.

What else can I try?

EasyControls version: 1.0.23 (previously 1.0.19)

Sad to here. The EasyControls versions should not be the problem. At least it was already reported as working.

I will try to recreate it when I find time (currently sick…)

So you think it is connected to a home assistant update?

Do others have the same problem?

Do you still can reach the homepage of the KWL via your browser?

Oh no, get well soon!

Yes, I had the same errors with 1.0.23 as with 1.0.19.

Since it was working until recently and then stopped, I assume it was an HA update. However, I can’t rule out other causes. But I can’t think of any other changes.

And yes, the KWL web interface is accessible in the browser.

I’m annoyed because I obviously found the solution myself once before, but I can’t remember how :slight_smile:

I have an additional update: As I wrote, I tried to cheat by adjusting the code in config_flow.py as follows.

This skipped the test_connection() function and allowed me to install the integration.

Unfortunately, no entities are created and the following error message appears in the log:

image

and this

image

Perhaps this will help. It appears at the time, HA tries to load the integration

PS: When I access the KWL interface with CURL, the response is ‘compressed’ and has to be decompressed first. But I think it has always been that way and you know that.