Repository: XKNX/xknx

Issue:
Climate operation mode set to Sleep after HA restart 
**Description of problem:**

- [ ] using xknx standalone
- [X] using Home-Assistant knx integration

**Version information:**
- xknx / Home-Assistant release with the issue: 2024.8.2
- last working xknx / Home-Assistant release (if known): Unsure, perhaps 2024.7.X

**Problem-relevant `configuration.yaml` entries (fill out even if it seems unimportant):**

![image](https://github.com/user-attachments/assets/346a83af-381a-432e-a277-9bd359ca0cee)

```
  climate:
    - name: Hallway
      temperature_step: 0.5
      temperature_address: 4/6/1
      target_temperature_state_address: 4/5/1
      setpoint_shift_address: 3/3/1
      setpoint_shift_state_address: 4/3/1
      operation_mode_address: 3/1/1
      controller_status_state_address: 4/0/1
      operation_modes:
        - Comfort
        - Standby
        - Night
        - Frost Protection
```


I've tried commenting out `operation_modes` but that didn't do anything apart from adding "None" as a mode.

On start I see it is reading `[0x21]` from the bus during startup, but I'm not sure what this value translates to since this is Jung Room controller for which years ago @farmio had worked hard to add `controller_status_state_address`:

```
2024-08-19 12:23:27.783 DEBUG (MainThread) [xknx.cemi] Incoming CEMI: <CEMIFrame code="L_DATA_IND" info="CEMIInfo("")" data="CEMILData(src_addr="IndividualAddress("1.1.1")" dst_addr="GroupAddress("4/0/1")" flags="1011110011100000" tpci="TDataGroup()" payload="<GroupValueResponse value="<DPTArray value="[0x21]" />" />")" />
```

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/XKNX/xknx/pull/1551?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.92%. Comparing base [(`566e3a8`)](https://app.codecov.io/gh/XKNX/xknx/commit/566e3a8e11f477bad1dae5af31baac4aa59d1b2e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`d1d060d`)](https://app.codecov.io/gh/XKNX/xknx/commit/d1d060d6ce80a7dfda95b91ed991bb02260b21b4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX).

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/XKNX/xknx/pull/1551/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://app.codecov.io/gh/XKNX/xknx/pull/1551?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1551   +/-   ##
=======================================
  Coverage   96.91%   96.92%           
=======================================
  Files         157      157           
  Lines       10554    10554           
=======================================
+ Hits        10228    10229    +1     
+ Misses        326      325    -1     
```

| [Files](https://app.codecov.io/gh/XKNX/xknx/pull/1551?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/dpt/dpt\_20.py](https://app.codecov.io/gh/XKNX/xknx/pull/1551?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_20.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0XzIwLnB5) | `100.00% <100.00%> (+0.86%)` | :arrow_up: |

</details>

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 329685b3..1c9cf927 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,10 @@ nav_order: 2
 
 # Changelog
 
+### DPT
+
+- Fix DPTHVACStatus inverted bit order
+
 # 3.1.0 DPT 1 2024-08-13
 
 ### DPT
diff --git a/xknx/dpt/dpt_20.py b/xknx/dpt/dpt_20.py
index aff5158b..f6ed63e4 100644
--- a/xknx/dpt/dpt_20.py
+++ b/xknx/dpt/dpt_20.py
@@ -158,23 +158,23 @@ class DPTHVACStatus(DPTComplex[HVACStatus]):
         raw = cls.validate_payload(payload)[0]
 
         mode: HVACOperationMode
-        if raw & 0b10000000:
+        if raw & 0x1:
             mode = HVACOperationMode.COMFORT
-        elif raw & 0b01000000:
+        elif raw & 0x2:
             mode = HVACOperationMode.STANDBY
-        elif raw & 0b00100000:
+        elif raw & 0x4:
             mode = HVACOperationMode.ECONOMY
-        elif raw & 0b00010000:
+        elif raw & 0x8:
             mode = HVACOperationMode.BUILDING_PROTECTION
         else:
             mode = HVACOperationMode.AUTO  # not sure if this is possible / valid
 
         return HVACStatus(
             mode=mode,
-            dew_point=bool(raw & 0b00001000),
-            heat_cool=HeatCool.HEAT if raw & 0b00000100 else HeatCool.COOL,
-            inactive=bool(raw & 0b00000010),
-            frost_alarm=bool(raw & 0b00000001),
+            dew_point=bool(raw & 0x10),
+            heat_cool=HeatCool.HEAT if raw & 0x20 else HeatCool.COOL,
+            inactive=bool(raw & 0x40),
+            frost_alarm=bool(raw & 0x80),
         )
 
     @classmethod
@@ -182,19 +182,19 @@ class DPTHVACStatus(DPTComplex[HVACStatus]):
         """Serialize to KNX/IP raw data."""
         raw = 0
         if value.mode is HVACOperationMode.COMFORT:
-            raw |= 0b10000000
+            raw |= 0x1
         elif value.mode is HVACOperationMode.STANDBY:
-            raw |= 0b01000000
+            raw |= 0x2
         elif value.mode is HVACOperationMode.ECONOMY:
-            raw |= 0b00100000
+            raw |= 0x4
         elif value.mode is HVACOperationMode.BUILDING_PROTECTION:
-            raw |= 0b00010000
+            raw |= 0x8
         if value.dew_point:
-            raw |= 0b00001000
+            raw |= 0x10
         if value.heat_cool is HeatCool.HEAT:
-            raw |= 0b00000100
+            raw |= 0x20
         if value.inactive:
-            raw |= 0b00000010
+            raw |= 0x40
         if value.frost_alarm:
-            raw |= 0b00000001
+            raw |= 0x80
         return DPTArray(raw)


Test patch:
diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py
index 62004811..f4132c03 100644
--- a/test/devices_tests/climate_test.py
+++ b/test/devices_tests/climate_test.py
@@ -308,16 +308,19 @@ class TestClimate:
         climate_mode.process(
             Telegram(
                 destination_address=GroupAddress("1/2/4"),
-                payload=GroupValueWrite(DPTArray((0b10000000,))),
+                payload=GroupValueWrite(DPTArray((0x01,))),
             )
         )
+        assert climate_mode.controller_mode == HVACControllerMode.COOL
+        assert climate_mode.operation_mode == HVACOperationMode.COMFORT
+
         # controller mode
         await climate_mode.set_controller_mode(HVACControllerMode.HEAT)
         assert xknx.telegrams.qsize() == 1
         telegram = xknx.telegrams.get_nowait()
         assert telegram == Telegram(
             destination_address=GroupAddress("1/2/4"),
-            payload=GroupValueWrite(DPTArray((0b10000100,))),
+            payload=GroupValueWrite(DPTArray((0x21,))),
         )
         climate_mode.process(telegram)  # process to have internal value updated
         # operation mode
@@ -326,7 +329,7 @@ class TestClimate:
         telegram = xknx.telegrams.get_nowait()
         assert telegram == Telegram(
             destination_address=GroupAddress("1/2/4"),
-            payload=GroupValueWrite(DPTArray((0b01000100,))),
+            payload=GroupValueWrite(DPTArray((0x22,))),
         )
 
     async def test_set_operation_mode_with_separate_addresses(self):
diff --git a/test/dpt_tests/dpt_20_hvac_mode_test.py b/test/dpt_tests/dpt_20_hvac_mode_test.py
index aa12220b..096836a8 100644
--- a/test/dpt_tests/dpt_20_hvac_mode_test.py
+++ b/test/dpt_tests/dpt_20_hvac_mode_test.py
@@ -202,7 +202,7 @@ class TestDPTHVACStatus:
                     inactive=False,
                     frost_alarm=False,
                 ),
-                (0b10000100,),
+                (0x21,),
             ),
             (
                 HVACStatus(
@@ -212,8 +212,8 @@ class TestDPTHVACStatus:
                     inactive=False,
                     frost_alarm=False,
                 ),
-                (0b10000000,),
-            ),  # min values
+                (0x01,),
+            ),
             (
                 HVACStatus(
                     mode=HVACOperationMode.ECONOMY,
@@ -222,7 +222,7 @@ class TestDPTHVACStatus:
                     inactive=False,
                     frost_alarm=True,
                 ),
-                (0b00101001,),
+                (0x94,),
             ),
         ],
     )
@@ -243,7 +243,7 @@ class TestDPTHVACStatus:
                     "inactive": False,
                     "frost_alarm": False,
                 },
-                (0b10000100,),
+                (0x21,),
             ),
             (
                 {
@@ -253,7 +253,7 @@ class TestDPTHVACStatus:
                     "inactive": True,
                     "frost_alarm": False,
                 },
-                (0b01000010,),
+                (0x42,),
             ),
         ],
     )