3 Commits

Author SHA1 Message Date
91f51cb414 Fix relative color temperature steps 2026-02-15 22:02:36 +01:00
1eb283112c Improve relative dimming decode 2026-02-15 21:51:50 +01:00
60b436f79f Use mired color temperature service 2026-02-15 21:46:50 +01:00
5 changed files with 146 additions and 11 deletions

View File

@@ -46,7 +46,10 @@ Completed:
- Relative dimming now repeats steps until a stop telegram is received.
- Relative color temperature steps (DPT 3.007) added for lights.
- Relative color temperature control wired into light schema, UI order adjusted, and KNX color temperature types aligned.
- Project version set to 0.0.28 and `CHANGELOG.md` maintained.
- Color temperature service calls now use mireds for better compatibility.
- Relative dimming/color temperature decoding improved for control/stepcode payloads.
- 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`

View File

@@ -1,5 +1,14 @@
# 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.
## 0.0.29 - 2026-02-15
- Use mired-based HA color temperature service calls for better device compatibility.
## 0.0.28 - 2026-02-15
- Add relative color temperature control (DPT 3.007), reorder relative dimming field in UI, and align color temperature KNX types.

View File

@@ -74,10 +74,10 @@ Only state addresses expose an `invert outgoing` toggle to flip KNX payloads.
Notes:
- For XY color, the bridge sends the brightness as the Y (luminance) component.
- Relative dimming (DPT 3.007) maps KNX step values to small `brightness_step_pct` changes in Home Assistant.
- Relative dimming (DPT 3.007) maps KNX step values (control/stepcode) to small `brightness_step_pct` changes in Home Assistant.
- For relative dimming, the bridge repeats steps until a KNX stop telegram (0 or 8) is received.
- Relative color temperature (DPT 3.007) adjusts Kelvin in the same start/stop pattern.
- Color temperature mode must match the KNX telegram DPT: `relative` for 5.001, `absolute` for 7.600 (2-byte unsigned), `absolute_float` for DPT 9 (2-byte float).
- Color temperature mode must match the KNX telegram DPT: `relative` for 5.001, `absolute` for 7.600 (2-byte unsigned), `absolute_float` for DPT 9 (2-byte float). The bridge sends HA color temperature using `color_temp` (mireds) for maximum compatibility.
## Notes
- For DPT 1.008 (Up/Down), the bridge treats `0 = Up/Open` and `1 = Down/Close`.
@@ -112,5 +112,5 @@ Notes:
- Advanced DPT mapping options and inversion settings.
## Versioning and Releases
- Current version: 0.0.28
- Current version: 0.0.31
- `CHANGELOG.md` lists versions with the newest entries at the top.

View File

@@ -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",
)
@@ -1167,12 +1167,24 @@ class BridgeManager:
await self._call_light_service(
port.entity_id,
"turn_on",
{ATTR_COLOR_TEMP_KELVIN: int(round(kelvin))},
{"color_temp": _kelvin_to_mireds(kelvin)},
)
return
if action == "relative_dimming":
value = _extract_event_value(event)
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
if value in (0, 8):
@@ -1188,7 +1200,19 @@ class BridgeManager:
return
if action == "relative_color_temperature":
value = _extract_event_value(event)
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
if value in (0, 8):
@@ -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:
@@ -1477,7 +1516,7 @@ class BridgeManager:
await self._call_light_service(
port.entity_id,
"turn_on",
{ATTR_COLOR_TEMP_KELVIN: int(round(next_kelvin))},
{"color_temp": _kelvin_to_mireds(next_kelvin)},
)
await asyncio.sleep(0.3)
@@ -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:
@@ -1511,6 +1580,42 @@ def _extract_event_value(event: Event) -> int | None:
return 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):
control = value.get("control")
step = value.get("stepcode", value.get("step_code"))
if control is not None and step is not None:
try:
return bool(int(control)), int(step) & 0x07
except (TypeError, ValueError):
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:
return bool(int(data[0])), int(data[1]) & 0x07
except (TypeError, ValueError):
return None
return None
def _clean_address(address: Any) -> str | None:
if not address:
return None
@@ -1803,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

View File

@@ -1,7 +1,7 @@
{
"domain": "ha_knx_bridge",
"name": "HA KNX Bridge",
"version": "0.0.28",
"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",