diff --git a/.idea/PROJECT_STATE.md b/.idea/PROJECT_STATE.md index f01ff9d..323f64c 100644 --- a/.idea/PROJECT_STATE.md +++ b/.idea/PROJECT_STATE.md @@ -48,7 +48,8 @@ Completed: - Relative color temperature control wired into light schema, UI order adjusted, and KNX color temperature types aligned. - Color temperature service calls now use mireds for better compatibility. - Relative dimming/color temperature decoding improved for control/stepcode payloads. -- Project version set to 0.0.30 and `CHANGELOG.md` maintained. +- Relative color temperature adjustment now accepts step_code 0 as a single-step and registers control_dimming event types for relative dimming/CT. +- Project version set to 0.0.31 and `CHANGELOG.md` maintained. Files created: - `custom_components/ha_knx_bridge/__init__.py` diff --git a/CHANGELOG.md b/CHANGELOG.md index d08b387..3b41aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.0.31 - 2026-02-15 +- Treat DPT 3.007 step_code 0 as a single-step for relative dimming/CT and register control_dimming event types. + ## 0.0.30 - 2026-02-15 - Improve relative dimming/color temperature decoding for control/stepcode payloads. diff --git a/README.md b/README.md index f38baa5..4655a53 100644 --- a/README.md +++ b/README.md @@ -112,5 +112,5 @@ Notes: - Advanced DPT mapping options and inversion settings. ## Versioning and Releases -- Current version: 0.0.30 +- Current version: 0.0.31 - `CHANGELOG.md` lists versions with the newest entries at the top. diff --git a/custom_components/ha_knx_bridge/bridge.py b/custom_components/ha_knx_bridge/bridge.py index 9205b66..10d5eac 100644 --- a/custom_components/ha_knx_bridge/bridge.py +++ b/custom_components/ha_knx_bridge/bridge.py @@ -678,13 +678,13 @@ class BridgeManager: ) self._register_knx_light_command( port.relative_color_temperature_address, - None, + "control_dimming", port, "relative_color_temperature", ) self._register_knx_light_command( port.relative_dimming_address, - None, + "control_dimming", port, "relative_dimming", ) @@ -1172,6 +1172,18 @@ class BridgeManager: return if action == "relative_dimming": + dim_info = _extract_dimming_info(event) + if dim_info is not None: + control, step_code = dim_info + if step_code == 0: + direction = "up" if control else "down" + if port.entity_id in self._light_dimming_tasks: + self._stop_light_dimming(port.entity_id) + return + self._apply_light_dimming_step( + port.entity_id, direction, 1 + ) + return value = _extract_dimming_value(event) if value is None: return @@ -1188,6 +1200,18 @@ class BridgeManager: return if action == "relative_color_temperature": + dim_info = _extract_dimming_info(event) + if dim_info is not None: + control, step_code = dim_info + if step_code == 0: + direction = "up" if control else "down" + if port.entity_id in self._light_ct_tasks: + self._stop_light_ct_adjust(port.entity_id) + return + self._apply_light_ct_step_once( + port, direction, 1 + ) + return value = _extract_dimming_value(event) if value is None: return @@ -1448,6 +1472,21 @@ class BridgeManager: _runner() ) + def _apply_light_dimming_step( + self, entity_id: str, direction: str, step_code: int + ) -> None: + percent = _relative_dimming_percent(step_code) + if percent is None or percent <= 0: + return + + async def _runner() -> None: + step = percent if direction == "up" else -percent + await self._call_light_service( + entity_id, "turn_on", {"brightness_step_pct": step} + ) + + self.hass.async_create_task(_runner()) + def _stop_light_dimming(self, entity_id: str) -> None: task = self._light_dimming_tasks.pop(entity_id, None) if task is not None: @@ -1485,6 +1524,36 @@ class BridgeManager: _runner() ) + def _apply_light_ct_step_once( + self, port: LightPort, direction: str, step_code: int + ) -> None: + percent = _relative_dimming_percent(step_code) + if percent is None or percent <= 0: + return + + async def _runner() -> None: + current_kelvin = _current_color_temp_kelvin(self.hass, port) + if current_kelvin is None: + current_kelvin = _light_min_kelvin(port) + delta = (_light_max_kelvin(port) - _light_min_kelvin(port)) * ( + percent / 100 + ) + if direction == "up": + next_kelvin = current_kelvin + delta + else: + next_kelvin = current_kelvin - delta + next_kelvin = min( + max(next_kelvin, _light_min_kelvin(port)), + _light_max_kelvin(port), + ) + await self._call_light_service( + port.entity_id, + "turn_on", + {"color_temp": _kelvin_to_mireds(next_kelvin)}, + ) + + self.hass.async_create_task(_runner()) + def _stop_light_ct_adjust(self, entity_id: str) -> None: task = self._light_ct_tasks.pop(entity_id, None) if task is not None: @@ -1512,6 +1581,14 @@ def _extract_event_value(event: Event) -> int | None: def _extract_dimming_value(event: Event) -> int | None: + info = _extract_dimming_info(event) + if info is not None: + control, step_code = info + return ((1 if control else 0) << 3) | (step_code & 0x07) + return _extract_event_value(event) + + +def _extract_dimming_info(event: Event) -> tuple[bool, int] | None: if "value" in event.data: value = event.data["value"] if isinstance(value, dict): @@ -1519,20 +1596,24 @@ def _extract_dimming_value(event: Event) -> int | None: step = value.get("stepcode", value.get("step_code")) if control is not None and step is not None: try: - control_bit = 1 if int(control) else 0 - step_code = int(step) & 0x07 - return (control_bit << 3) | step_code + return bool(int(control)), int(step) & 0x07 except (TypeError, ValueError): - pass + return None + if isinstance(value, (int, float)): + raw = int(value) + return bool(raw & 0x08), raw & 0x07 + if isinstance(value, (list, tuple)) and len(value) >= 2: + try: + return bool(int(value[0])), int(value[1]) & 0x07 + except (TypeError, ValueError): + return None data = event.data.get("data") if isinstance(data, (list, tuple)) and len(data) >= 2: try: - control_bit = 1 if int(data[0]) else 0 - step_code = int(data[1]) & 0x07 - return (control_bit << 3) | step_code + return bool(int(data[0])), int(data[1]) & 0x07 except (TypeError, ValueError): return None - return _extract_event_value(event) + return None def _clean_address(address: Any) -> str | None: @@ -1827,6 +1908,24 @@ def _relative_dimming_step(value: int) -> tuple[str, int] | None: return direction, percent +def _relative_dimming_percent(step_code: int) -> int | None: + percent_map = { + 1: 10, + 2: 8, + 3: 6, + 4: 4, + 5: 3, + 6: 2, + 7: 1, + } + step = int(step_code) + if step <= 0: + step = 1 + if step > 7: + step = 7 + return percent_map.get(step) + + def _map_scalar_value(value: Any) -> int | None: if isinstance(value, bool): return 1 if value else 0 diff --git a/custom_components/ha_knx_bridge/manifest.json b/custom_components/ha_knx_bridge/manifest.json index aaa0f24..630e0fc 100644 --- a/custom_components/ha_knx_bridge/manifest.json +++ b/custom_components/ha_knx_bridge/manifest.json @@ -1,7 +1,7 @@ { "domain": "ha_knx_bridge", "name": "HA KNX Bridge", - "version": "0.0.30", + "version": "0.0.31", "config_flow": true, "documentation": "https://github.com/bahmcloud/HA-KNX-Bridge", "issue_tracker": "https://github.com/bahmcloud/HA-KNX-Bridge/issues",