Repository: XKNX/xknx (python)

Issue:
Climate: Autosense setpoint shift mode
For climate setpoint_shift there are 2 modes
- 1 byte signed int DPT 6.010 
- 2 byte float DPT 9.002

Changing the target temperature requires to know the value of setpoint_shift 
https://github.com/XKNX/xknx/blob/a574a49143e4c3d4a3d329e91b96e236752db3f6/xknx/devices/climate.py#L195 
So we also know the payload length and could assign the according DPT class in `RemoteValueSetpointShift.from_knx()` instead of configuring it manually 
https://github.com/XKNX/xknx/blob/a574a49143e4c3d4a3d329e91b96e236752db3f6/xknx/devices/climate.py#L97

Pull request description:
Autosense Climate setpoint_shift_mode from first incoming payload length
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->
Autosense Climate setpoint_shift_mode from first incoming payload length

closes #686

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

## Checklist:

- [x] I have performed a self-review of my own code
- [x] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


Patch:
diff --git a/changelog.md b/changelog.md
index 2316a2bb..f80fdf94 100644
--- a/changelog.md
+++ b/changelog.md
@@ -4,6 +4,7 @@
 
 ### Devices
 
+- Climate: make `setpoint_shift_mode` optional. When `None` assign its DPT from the first incoming payload.
 - Light: Support individual color lights without switch object
 
 ## 0.18.1 Internal group addresses 2021-04-23
diff --git a/docs/climate.md b/docs/climate.md
index 33822f83..0084631f 100644
--- a/docs/climate.md
+++ b/docs/climate.md
@@ -15,18 +15,18 @@ Climate are representations of KNX HVAC/Climate controls.
 
 - `xknx` is the XKNX object.
 - `name` is the name of the object.
-- `group_address_temperature` KNX address of current room temperature.
-- `group_address_target_temperature` KNX address for setting the target temperature if setpoint shift is not supported.
-- `group_address_target_temperature_state` KNX address for reading the target temperature from the KNX bus. Used in for setpoint_shift calculations as base temperature.
-- `group_address_setpoint_shift` KNX address to set setpoint_shift (base temperature deviation).
-- `group_address_setpoint_shift_state` KNX address to read current setpoint_shift.
-- `setpoint_shift_mode` Enum for setpoint_shift payload encoding. Can be *DPT 6.010* or *DPT 9.002*. Default: SetpointShiftMode.DPT6010
+- `group_address_temperature` KNX address of current room temperature. *DPT 9.001*
+- `group_address_target_temperature` KNX address for setting the target temperature if setpoint shift is not supported. *DPT 9.001*
+- `group_address_target_temperature_state` KNX address for reading the target temperature from the KNX bus. Used in for setpoint_shift calculations as base temperature. *DPT 9.001*
+- `group_address_setpoint_shift` KNX address to set setpoint_shift (base temperature deviation). *DPT 6.010* or *DPT 9.002*
+- `group_address_setpoint_shift_state` KNX address to read current setpoint_shift. *DPT 6.010* or *DPT 9.002*
+- `setpoint_shift_mode` SetpointShiftMode Enum for setpoint_shift payload encoding. When `None` it is infered from first incoming payload. Default: `None`
 - `setpoint_shift_max` Maximum value for setpoint_shift.
 - `setpoint_shift_min` Minimum value for setpoint_shift.
-- `temperature_step` Set the multiplier for setpoint_shift (*DPT 6*) calculations and the step size for HA UI-elements.
-- `group_address_on_off` KNX address for turning climate device on or off.
-- `group_address_on_off_state` KNX address for reading the on/off state.
-- `on_off_invert` Invert on/off.
+- `temperature_step` Set the multiplier for setpoint_shift calculations when DPT 6.010 is used.
+- `group_address_on_off` KNX address for turning climate device on or off. *DPT 1*
+- `group_address_on_off_state` KNX address for reading the on/off state. *DPT 1*
+- `on_off_invert` Invert on/off. Default: `False`
 - `max_temp` Maximum value for target temperature.
 - `min_temp` Minimum value for target temperature.
 - `mode` ClimateMode instance for this climate device
diff --git a/xknx/devices/climate.py b/xknx/devices/climate.py
index 67df602e..d5a2ff95 100644
--- a/xknx/devices/climate.py
+++ b/xknx/devices/climate.py
@@ -6,7 +6,6 @@ Module for managing the climate within a room.
 """
 from __future__ import annotations
 
-from enum import Enum
 import logging
 from typing import TYPE_CHECKING, Any, Iterator
 
@@ -17,6 +16,7 @@ from xknx.remote_value import (
     RemoteValueSwitch,
     RemoteValueTemp,
 )
+from xknx.remote_value.remote_value_setpoint_shift import SetpointShiftMode
 
 from .climate_mode import ClimateMode
 from .device import Device, DeviceCallbackType
@@ -30,17 +30,9 @@ if TYPE_CHECKING:
 logger = logging.getLogger("xknx.log")
 
 
-class SetpointShiftMode(Enum):
-    """Enum for setting the setpoint shift mode."""
-
-    DPT6010 = 1
-    DPT9002 = 2
-
-
 DEFAULT_SETPOINT_SHIFT_MAX = 6
 DEFAULT_SETPOINT_SHIFT_MIN = -6
 DEFAULT_TEMPERATURE_STEP = 0.1
-DEFAULT_SETPOINT_SHIFT_MODE = SetpointShiftMode.DPT6010
 
 
 class Climate(Device):
@@ -55,7 +47,7 @@ class Climate(Device):
         group_address_target_temperature_state: GroupAddressesType | None = None,
         group_address_setpoint_shift: GroupAddressesType | None = None,
         group_address_setpoint_shift_state: GroupAddressesType | None = None,
-        setpoint_shift_mode: SetpointShiftMode = DEFAULT_SETPOINT_SHIFT_MODE,
+        setpoint_shift_mode: SetpointShiftMode | None = None,
         setpoint_shift_max: float = DEFAULT_SETPOINT_SHIFT_MAX,
         setpoint_shift_min: float = DEFAULT_SETPOINT_SHIFT_MIN,
         temperature_step: float = DEFAULT_TEMPERATURE_STEP,
@@ -94,24 +86,15 @@ class Climate(Device):
             after_update_cb=self.after_update,
         )
 
-        self._setpoint_shift: RemoteValueTemp | RemoteValueSetpointShift
-        if setpoint_shift_mode == SetpointShiftMode.DPT9002:
-            self._setpoint_shift = RemoteValueTemp(
-                xknx,
-                group_address_setpoint_shift,
-                group_address_setpoint_shift_state,
-                device_name=self.name,
-                after_update_cb=self.after_update,
-            )
-        else:
-            self._setpoint_shift = RemoteValueSetpointShift(
-                xknx,
-                group_address_setpoint_shift,
-                group_address_setpoint_shift_state,
-                device_name=self.name,
-                after_update_cb=self.after_update,
-                setpoint_shift_step=self.temperature_step,
-            )
+        self._setpoint_shift = RemoteValueSetpointShift(
+            xknx,
+            group_address_setpoint_shift,
+            group_address_setpoint_shift_state,
+            device_name=self.name,
+            after_update_cb=self.after_update,
+            setpoint_shift_mode=setpoint_shift_mode,
+            setpoint_shift_step=self.temperature_step,
+        )
 
         self.supports_on_off = (
             group_address_on_off is not None or group_address_on_off_state is not None
@@ -133,12 +116,10 @@ class Climate(Device):
 
     def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]:
         """Iterate the devices RemoteValue classes."""
-        yield from (
-            self.temperature,
-            self.target_temperature,
-            self._setpoint_shift,
-            self.on,
-        )
+        yield self.temperature
+        yield self.target_temperature
+        yield self._setpoint_shift
+        yield self.on
 
     def create_temperature_sensors(self) -> None:
         """Create temperature sensors."""
diff --git a/xknx/remote_value/remote_value_setpoint_shift.py b/xknx/remote_value/remote_value_setpoint_shift.py
index f46fb3ad..61b6b851 100644
--- a/xknx/remote_value/remote_value_setpoint_shift.py
+++ b/xknx/remote_value/remote_value_setpoint_shift.py
@@ -5,9 +5,11 @@ DPT 6.010.
 """
 from __future__ import annotations
 
+from enum import Enum
 from typing import TYPE_CHECKING
 
-from xknx.dpt import DPTArray, DPTBinary, DPTValue1Count
+from xknx.dpt import DPTArray, DPTBinary, DPTTemperature, DPTValue1Count
+from xknx.exceptions import ConversionError
 
 from .remote_value import AsyncCallbackType, GroupAddressesType, RemoteValue
 
@@ -15,6 +17,13 @@ if TYPE_CHECKING:
     from xknx.xknx import XKNX
 
 
+class SetpointShiftMode(Enum):
+    """Enum for setting the setpoint shift mode."""
+
+    DPT6010 = DPTValue1Count
+    DPT9002 = DPTTemperature
+
+
 class RemoteValueSetpointShift(RemoteValue[DPTArray, float]):
     """Abstraction for remote value of KNX DPT 6.010."""
 
@@ -25,6 +34,7 @@ class RemoteValueSetpointShift(RemoteValue[DPTArray, float]):
         group_address_state: GroupAddressesType | None = None,
         device_name: str | None = None,
         after_update_cb: AsyncCallbackType | None = None,
+        setpoint_shift_mode: SetpointShiftMode | None = None,
         setpoint_shift_step: float = 0.1,
     ):
         """Initialize RemoteValueSetpointShift class."""
@@ -37,23 +47,40 @@ class RemoteValueSetpointShift(RemoteValue[DPTArray, float]):
             after_update_cb=after_update_cb,
         )
 
+        self.dpt_class: type[DPTValue1Count | DPTTemperature] | None = (
+            setpoint_shift_mode.value if setpoint_shift_mode is not None else None
+        )
         self.setpoint_shift_step = setpoint_shift_step
 
     def payload_valid(self, payload: DPTArray | DPTBinary | None) -> DPTArray | None:
         """Test if telegram payload may be parsed."""
-        # pylint: disable=no-self-use
-        return (
-            payload
-            if isinstance(payload, DPTArray) and len(payload.value) == 1
-            else None
-        )
+        if not isinstance(payload, DPTArray):
+            return None
+        payload_length = len(payload.value)
+        if self.dpt_class is None:
+            if payload_length == DPTTemperature.payload_length:
+                self.dpt_class = DPTTemperature
+            elif payload_length == DPTValue1Count.payload_length:
+                self.dpt_class = DPTValue1Count
+            else:
+                return None
+        return payload if payload_length == self.dpt_class.payload_length else None
 
     def to_knx(self, value: float) -> DPTArray:
         """Convert value to payload."""
-        converted_value = int(value / self.setpoint_shift_step)
-        return DPTArray(DPTValue1Count.to_knx(converted_value))
+        if self.dpt_class is None:
+            raise ConversionError(
+                "Setpoint shift DPT not initialized for %s" % self.device_name
+            )
+        if self.dpt_class == DPTValue1Count:
+            converted_value = int(value / self.setpoint_shift_step)
+            return DPTArray(DPTValue1Count.to_knx(converted_value))
+        return DPTArray(DPTTemperature.to_knx(value))
 
     def from_knx(self, payload: DPTArray) -> float:
         """Convert current payload to value."""
-        converted_payload = DPTValue1Count.from_knx(payload.value)
-        return converted_payload * self.setpoint_shift_step
+        assert self.dpt_class is not None  # checked by payload_valid() from process()
+        payload_value = self.dpt_class.from_knx(payload.value)
+        if self.dpt_class == DPTValue1Count:
+            return payload_value * self.setpoint_shift_step
+        return payload_value


Test patch:
diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py
index 3038d4a4..cd4d0b12 100644
--- a/test/devices_tests/climate_test.py
+++ b/test/devices_tests/climate_test.py
@@ -16,7 +16,7 @@ from xknx.dpt import (
     DPTValue1Count,
 )
 from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
-from xknx.exceptions import CouldNotParseTelegram, DeviceIllegalValue
+from xknx.exceptions import ConversionError, CouldNotParseTelegram, DeviceIllegalValue
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueRead, GroupValueWrite
 
@@ -144,6 +144,7 @@ class TestClimate:
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
             group_address_setpoint_shift_state="1/2/4",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
         )
 
         after_update_callback = Mock()
@@ -409,7 +410,12 @@ class TestClimate:
         climate1 = Climate(xknx, "TestClimate")
         assert not climate1.initialized_for_setpoint_shift_calculations
 
-        climate2 = Climate(xknx, "TestClimate", group_address_setpoint_shift="1/2/3")
+        climate2 = Climate(
+            xknx,
+            "TestClimate",
+            group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
+        )
         assert not climate2.initialized_for_setpoint_shift_calculations
         await climate2.set_setpoint_shift(4)
         await xknx.devices.process(xknx.telegrams.get_nowait())
@@ -420,6 +426,7 @@ class TestClimate:
             "TestClimate",
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
         )
         await climate3.set_setpoint_shift(4)
         await xknx.devices.process(xknx.telegrams.get_nowait())
@@ -429,6 +436,66 @@ class TestClimate:
         await xknx.devices.process(xknx.telegrams.get_nowait())
         assert climate3.initialized_for_setpoint_shift_calculations
 
+    async def test_setpoint_shift_mode_autosensing(self):
+        """Test autosensing setpoint_shift_mode."""
+
+        xknx = XKNX()
+        climate_dpt6 = Climate(
+            xknx,
+            "TestClimate",
+            group_address_temperature="1/2/1",
+            group_address_target_temperature_state="1/2/2",
+            group_address_setpoint_shift="1/2/3",
+        )
+        climate_dpt6.target_temperature.value = 23.00
+
+        # uninitialized
+        with pytest.raises(ConversionError):
+            await climate_dpt6.set_setpoint_shift(1)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray(DPTValue1Count.to_knx(-4))),
+        )
+        await climate_dpt6.process(telegram)
+        assert climate_dpt6.initialized_for_setpoint_shift_calculations
+
+        await climate_dpt6.set_setpoint_shift(1)
+        _telegram = xknx.telegrams.get_nowait()
+        # DPTValue1Count is used for outgoing
+        assert _telegram == Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray(10)),  # 1 / 0.1 setpoint_shift_step
+        )
+
+        climate_dpt9 = Climate(
+            xknx,
+            "TestClimate",
+            group_address_temperature="1/2/1",
+            group_address_target_temperature_state="1/2/2",
+            group_address_setpoint_shift="1/2/3",
+        )
+        climate_dpt9.target_temperature.value = 23.00
+
+        # uninitialized
+        with pytest.raises(ConversionError):
+            await climate_dpt9.set_setpoint_shift(1)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray(DPTTemperature.to_knx(-4))),
+        )
+        await climate_dpt9.process(telegram)
+        assert climate_dpt9.initialized_for_setpoint_shift_calculations
+
+        await climate_dpt9.set_setpoint_shift(1)
+        _telegram = xknx.telegrams.get_nowait()
+        # DPTTemperature is used for outgoing
+        assert _telegram == Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray(DPTTemperature.to_knx(1))),
+        )
+
     #
     # TEST for unitialized target_temperature_min/target_temperature_max
     #
@@ -460,6 +527,7 @@ class TestClimate:
             "TestClimate",
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
             max_temp="42",
             min_temp="3",
         )
@@ -485,6 +553,7 @@ class TestClimate:
             "TestClimate",
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
         )
 
         await climate.set_setpoint_shift(3)
@@ -562,6 +631,7 @@ class TestClimate:
             "TestClimate",
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
         )
 
         await climate.set_setpoint_shift(1)
@@ -639,6 +709,7 @@ class TestClimate:
             "TestClimate",
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
             temperature_step=0.5,
             setpoint_shift_max=10,
             setpoint_shift_min=-10,
@@ -697,6 +768,7 @@ class TestClimate:
             group_address_target_temperature_state="1/2/1",
             group_address_target_temperature="1/2/2",
             group_address_setpoint_shift="1/2/3",
+            setpoint_shift_mode=SetpointShiftMode.DPT6010,
         )
 
         await climate.set_target_temperature(21.00)
diff --git a/test/remote_value_tests/remote_value_setepoint_shift_test.py b/test/remote_value_tests/remote_value_setepoint_shift_test.py
new file mode 100644
index 00000000..d6f3852b
--- /dev/null
+++ b/test/remote_value_tests/remote_value_setepoint_shift_test.py
@@ -0,0 +1,119 @@
+"""Unit test for RemoteValueSensor objects."""
+import pytest
+from xknx import XKNX
+from xknx.dpt import DPTArray, DPTBinary, DPTTemperature, DPTValue1Count
+from xknx.exceptions import ConversionError
+from xknx.remote_value.remote_value_setpoint_shift import (
+    RemoteValueSetpointShift,
+    SetpointShiftMode,
+)
+
+
+class TestRemoteValueSetpointShift:
+    """Test class for RemoteValueSensor objects."""
+
+    def test_payload_valid_mode_assignment(self):
+        """Test if setpoint_shift_mode is assigned properly by payload length."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(xknx=xknx)
+        dpt_6_payload = DPTArray(DPTValue1Count.to_knx(1))
+        dpt_9_payload = DPTArray(DPTTemperature.to_knx(1))
+
+        assert remote_value.payload_valid(DPTBinary(0)) is None
+        assert remote_value.payload_valid(DPTArray((0, 1, 2))) is None
+
+        # DPT 6 - payload_length 1
+        assert remote_value.dpt_class is None
+        assert remote_value.payload_valid(dpt_6_payload) == dpt_6_payload
+        assert remote_value.dpt_class == SetpointShiftMode.DPT6010.value
+        #   DPT 9 is invalid now
+        assert remote_value.payload_valid(dpt_9_payload) is None
+
+        remote_value.dpt_class = None
+        # DPT 9 - payload_length 2
+        assert remote_value.payload_valid(dpt_9_payload) == dpt_9_payload
+        assert remote_value.dpt_class == SetpointShiftMode.DPT9002.value
+        #   DPT 6 is invalid now
+        assert remote_value.payload_valid(dpt_6_payload) is None
+
+    def test_payload_valid_preassigned_mode(self):
+        """Test if setpoint_shift_mode is assigned properly by payload length."""
+        xknx = XKNX()
+        remote_value_6 = RemoteValueSetpointShift(
+            xknx=xknx, setpoint_shift_mode=SetpointShiftMode.DPT6010
+        )
+        remote_value_9 = RemoteValueSetpointShift(
+            xknx=xknx, setpoint_shift_mode=SetpointShiftMode.DPT9002
+        )
+        dpt_6_payload = DPTArray(DPTValue1Count.to_knx(1))
+        dpt_9_payload = DPTArray(DPTTemperature.to_knx(1))
+
+        assert remote_value_6.dpt_class == DPTValue1Count
+        assert remote_value_6.payload_valid(None) is None
+        assert remote_value_6.payload_valid(dpt_9_payload) is None
+        assert remote_value_6.payload_valid(DPTArray((1, 2, 3, 4))) is None
+        assert remote_value_6.payload_valid(DPTBinary(1)) is None
+        assert remote_value_6.payload_valid(dpt_6_payload) == dpt_6_payload
+
+        assert remote_value_9.dpt_class == DPTTemperature
+        assert remote_value_9.payload_valid(None) is None
+        assert remote_value_9.payload_valid(dpt_6_payload) is None
+        assert remote_value_9.payload_valid(DPTArray((1, 2, 3))) is None
+        assert remote_value_9.payload_valid(DPTBinary(1)) is None
+        assert remote_value_9.payload_valid(dpt_9_payload) == dpt_9_payload
+
+    def test_to_knx_uninitialized(self):
+        """Test to_knx raising ConversionError."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(xknx=xknx)
+
+        assert remote_value.dpt_class is None
+        with pytest.raises(ConversionError):
+            remote_value.to_knx(1)
+
+    def test_to_knx_dpt_6(self):
+        """Test to_knx returning DPT 6.010 payload."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(
+            xknx=xknx, setpoint_shift_mode=SetpointShiftMode.DPT6010
+        )
+        assert remote_value.setpoint_shift_step == 0.1
+        assert remote_value.to_knx(1) == DPTArray((10,))
+
+    def test_to_knx_dpt_9(self):
+        """Test to_knx returning DPT 9.002 payload."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(
+            xknx=xknx, setpoint_shift_mode=SetpointShiftMode.DPT9002
+        )
+        assert remote_value.to_knx(1) == DPTArray((0x00, 0x64))
+
+    def test_from_knx_uninitialized(self):
+        """Test from_knx for uninitialized setpoint_shift_mode."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(xknx=xknx)
+        with pytest.raises(AssertionError):
+            remote_value.from_knx(1)
+        # assign DPT 9.002 mode
+        remote_value.payload_valid(DPTArray((0x00, 0x64)))
+        assert remote_value.from_knx(DPTArray((0x07, 0xD0))) == 20
+        # wrong payload length raises, once assigned
+        with pytest.raises(ConversionError):
+            remote_value.from_knx(DPTArray((10,)))
+
+    def test_from_knx_dpt_6(self):
+        """Test from_knx for DPT 6.010 setpoint_shift_mode."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(
+            xknx=xknx, setpoint_shift_mode=SetpointShiftMode.DPT6010
+        )
+        assert remote_value.setpoint_shift_step == 0.1
+        assert remote_value.from_knx(DPTArray((10,))) == 1
+
+    def test_from_knx_dpt_9(self):
+        """Test from_knx for DPT 9.002 setpoint_shift_mode."""
+        xknx = XKNX()
+        remote_value = RemoteValueSetpointShift(
+            xknx=xknx, setpoint_shift_mode=SetpointShiftMode.DPT9002
+        )
+        assert remote_value.from_knx(DPTArray((0x00, 0x64))) == 1