File: dpt_242.py

package info (click to toggle)
python-xknx 3.10.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,044 kB
  • sloc: python: 40,087; javascript: 8,556; makefile: 32; sh: 12
file content (131 lines) | stat: -rw-r--r-- 4,250 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""Implementation of KNX XYY color data point type."""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any

from .dpt import DPTComplex, DPTComplexData
from .payload import DPTArray, DPTBinary


@dataclass(slots=True)
class XYYColor(DPTComplexData):
    """
    Representation of XY color with brightness.

    `color`: tuple(x-axis, y-axis) each 0..1; None if invalid.
    `brightness`: int 0..255; None if invalid.
    """

    color: tuple[float, float] | None = None
    brightness: int | None = None

    @classmethod
    def from_dict(cls, data: Mapping[str, Any]) -> XYYColor:
        """Init from a dictionary."""
        color = None
        brightness = data.get("brightness")
        x_axis = data.get("x_axis")
        y_axis = data.get("y_axis")

        if x_axis is not None and y_axis is not None:
            try:
                color = (float(x_axis), float(y_axis))
            except ValueError as err:
                raise ValueError(f"Invalid value for color axis: {err}") from err
        elif x_axis is not None or y_axis is not None:
            raise ValueError("Both x_axis and y_axis must be provided")

        if brightness is not None:
            try:
                brightness = int(brightness)
            except ValueError as err:
                raise ValueError(f"Invalid value for brightness: {err}") from err

        return cls(color=color, brightness=brightness)

    def as_dict(self) -> dict[str, int | float | None]:
        """Create a JSON serializable dictionary."""
        return {
            "x_axis": self.color[0] if self.color is not None else None,
            "y_axis": self.color[1] if self.color is not None else None,
            "brightness": self.brightness,
        }

    def __or__(self, other: XYYColor) -> XYYColor:
        """Merge two XYYColor objects using only valid values."""
        return XYYColor(
            color=other.color if other.color is not None else self.color,
            brightness=other.brightness
            if other.brightness is not None
            else self.brightness,
        )


class DPTColorXYY(DPTComplex[XYYColor]):
    """Abstraction for KNX 6 octet color xyY (DPT 242.600)."""

    data_type = XYYColor
    payload_type = DPTArray
    payload_length = 6
    dpt_main_number = 242
    dpt_sub_number = 600
    value_type = "color_xyy"

    @classmethod
    def from_knx(cls, payload: DPTArray | DPTBinary) -> XYYColor:
        """Parse/deserialize from KNX/IP raw data."""
        raw = cls.validate_payload(payload)

        x_axis_int = raw[0] << 8 | raw[1]
        y_axis_int = raw[2] << 8 | raw[3]
        brightness = raw[4]

        color_valid = raw[5] >> 1 & 0b1
        brightness_valid = raw[5] & 0b1

        return XYYColor(
            color=(
                # round to 5 digits for better readability but still preserving precision
                round(x_axis_int / 0xFFFF, 5),
                round(y_axis_int / 0xFFFF, 5),
            )
            if color_valid
            else None,
            brightness=brightness if brightness_valid else None,
        )

    @classmethod
    def _to_knx(cls, value: XYYColor) -> DPTArray:
        """Serialize to KNX/IP raw data."""
        x_axis, y_axis, brightness = 0, 0, 0
        color_valid = False
        brightness_valid = False

        if value.color is not None:
            for axis in value.color:
                if not 0 <= axis <= 1:
                    raise ValueError(
                        f"Color axis value out of range 0..1: {value.color}"
                    )
            x_axis, y_axis = (round(axis * 0xFFFF) for axis in value.color)
            color_valid = True

        if value.brightness is not None:
            brightness = value.brightness
            if not 0 <= brightness <= 255:
                raise ValueError(f"Brightness out of range 0..255: {brightness}")
            brightness_valid = True

        return DPTArray(
            (
                x_axis >> 8,
                x_axis & 0xFF,
                y_axis >> 8,
                y_axis & 0xFF,
                brightness,
                color_valid << 1 | brightness_valid,
            )
        )