phue2 0.0.1.dev279__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
phue2/exceptions.py ADDED
@@ -0,0 +1,22 @@
1
+ """Exceptions for the phue_modern library."""
2
+
3
+
4
+ class PhueException(Exception):
5
+ """Base exception for all Philips Hue related errors."""
6
+
7
+ def __init__(self, id: int, message: str):
8
+ self.id = id
9
+ self.message = message
10
+ super().__init__(f"Error {id}: {message}")
11
+
12
+
13
+ class PhueRegistrationException(PhueException):
14
+ """Exception raised when registration with the bridge fails."""
15
+
16
+ pass
17
+
18
+
19
+ class PhueRequestTimeout(PhueException):
20
+ """Exception raised when a request to the bridge times out."""
21
+
22
+ pass
phue2/group.py ADDED
@@ -0,0 +1,118 @@
1
+ """Group classes for controlling groups of Philips Hue lights."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from phue2.light import Light
7
+
8
+ logger = logging.getLogger("phue_modern")
9
+
10
+ if TYPE_CHECKING:
11
+ from phue2.bridge import Bridge
12
+
13
+
14
+ class Group(Light):
15
+ """A group of Hue lights, tracked as a group on the bridge
16
+
17
+ Example:
18
+
19
+ >>> b = Bridge()
20
+ >>> g1 = Group(b, 1)
21
+ >>> g1.hue = 50000 # all lights in that group turn blue
22
+ >>> g1.on = False # all will turn off
23
+
24
+ >>> g2 = Group(b, 'Kitchen') # you can also look up groups by name
25
+ >>> # will raise a LookupError if the name doesn't match
26
+ """
27
+
28
+ def __init__(self, bridge: "Bridge", group_id: int | str):
29
+ Light.__init__(self, bridge, 0) # Light ID will be overridden
30
+ self.light_id = None # not relevant for a group
31
+ self._any_on: bool | None = None
32
+ self._all_on: bool | None = None
33
+ self.group_id: int
34
+
35
+ try:
36
+ self.group_id = int(group_id)
37
+ except ValueError:
38
+ name = str(group_id)
39
+ groups = bridge.get_group()
40
+ for idnumber, info in groups.items():
41
+ if info["name"] == name:
42
+ self.group_id = int(idnumber)
43
+ break
44
+ else:
45
+ raise LookupError("Could not find a group by that name.")
46
+
47
+ # Wrapper functions for get/set through the bridge, adding support for
48
+ # remembering the transitiontime parameter if the user has set it
49
+ def _get(self, *args: Any, **kwargs: Any) -> Any:
50
+ return self.bridge.get_group(self.group_id, *args, **kwargs)
51
+
52
+ def _set(self, *args: Any, **kwargs: Any) -> Any:
53
+ # let's get basic group functionality working first before adding
54
+ # transition time...
55
+ if self.transitiontime is not None:
56
+ kwargs["transitiontime"] = self.transitiontime
57
+ logger.debug(
58
+ f"Setting with transitiontime = {self.transitiontime} ds = {float(self.transitiontime) / 10} s"
59
+ )
60
+
61
+ if (args[0] == "on" and args[1] is False) or (
62
+ kwargs.get("on", True) is False
63
+ ):
64
+ self._reset_bri_after_on = True
65
+ return self.bridge.set_group(self.group_id, *args, **kwargs)
66
+
67
+ @property
68
+ def name(self) -> str:
69
+ """Get or set the name of the light group [string]"""
70
+ return self._get("name")
71
+
72
+ @name.setter
73
+ def name(self, value: str) -> None:
74
+ old_name = self.name
75
+ self._name = value
76
+ logger.debug(f"Renaming light group from '{old_name}' to '{value}'")
77
+ self._set("name", self._name)
78
+
79
+ @property
80
+ def any_on(self) -> bool:
81
+ """If true at least one light in the group is on"""
82
+ self._any_on = self._get("any_on")
83
+ return self._any_on or False
84
+
85
+ @property
86
+ def all_on(self) -> bool:
87
+ """If true all lights in the group are on"""
88
+ self._all_on = self._get("all_on")
89
+ return self._all_on or False
90
+
91
+ @property
92
+ def lights(self) -> list[Light]:
93
+ """Return a list of all lights in this group"""
94
+ return [Light(self.bridge, int(light_id)) for light_id in self._get("lights")]
95
+
96
+ @lights.setter
97
+ def lights(self, value: list[int]) -> None:
98
+ """Change the lights that are in this group"""
99
+ logger.debug(f"Setting lights in group {self.group_id} to {str(value)}")
100
+ self._set("lights", value)
101
+
102
+
103
+ class AllLights(Group):
104
+ """All the Hue lights connected to your bridge
105
+
106
+ This makes use of the semi-documented feature that
107
+ "Group 0" of lights appears to be a group automatically
108
+ consisting of all lights. This is not returned by
109
+ listing the groups, but is accessible if you explicitly
110
+ ask for group 0.
111
+ """
112
+
113
+ def __init__(self, bridge: "Bridge | None" = None):
114
+ if bridge is None:
115
+ from .bridge import Bridge
116
+
117
+ bridge = Bridge()
118
+ Group.__init__(self, bridge, 0)
phue2/light.py ADDED
@@ -0,0 +1,301 @@
1
+ """Light class for controlling Philips Hue lights."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from phue2.bridge import Bridge
8
+
9
+ logger = logging.getLogger("phue_modern")
10
+
11
+
12
+ class Light:
13
+ """Hue Light object
14
+
15
+ Light settings can be accessed or set via the properties of this object.
16
+ """
17
+
18
+ def __init__(self, bridge: "Bridge", light_id: int):
19
+ self.bridge = bridge
20
+ self.light_id = light_id
21
+
22
+ self._name: str | None = None
23
+ self._on: bool | None = None
24
+ self._brightness: int | None = None
25
+ self._colormode: str | None = None
26
+ self._hue: int | None = None
27
+ self._saturation: int | None = None
28
+ self._xy: tuple[float, float] | None = None
29
+ self._colortemp: int | None = None
30
+ self._effect: str | None = None
31
+ self._alert: str | None = None
32
+ self.transitiontime: int | None = None # default
33
+ self._reset_bri_after_on: bool | None = None
34
+ self._reachable: bool | None = None
35
+ self._type: str | None = None
36
+
37
+ def __repr__(self) -> str:
38
+ # like default python repr function, but add light name
39
+ return f'<{self.__class__.__module__}.{self.__class__.__name__} object "{self.name}" at {hex(id(self))}>'
40
+
41
+ # Wrapper functions for get/set through the bridge, adding support for
42
+ # remembering the transitiontime parameter if the user has set it
43
+ def _get(self, *args: Any, **kwargs: Any) -> Any:
44
+ return self.bridge.get_light(self.light_id, *args, **kwargs)
45
+
46
+ def _set(self, *args: Any, **kwargs: Any) -> Any:
47
+ if self.transitiontime is not None:
48
+ kwargs["transitiontime"] = self.transitiontime
49
+ logger.debug(
50
+ f"Setting with transitiontime = {self.transitiontime} ds = {float(self.transitiontime) / 10} s"
51
+ )
52
+
53
+ if (args[0] == "on" and args[1] is False) or (
54
+ kwargs.get("on", True) is False
55
+ ):
56
+ self._reset_bri_after_on = True
57
+ return self.bridge.set_light(self.light_id, *args, **kwargs)
58
+
59
+ @property
60
+ def name(self) -> str:
61
+ """Get or set the name of the light [string]"""
62
+ fetched_name = self._get("name")
63
+ assert isinstance(fetched_name, str), (
64
+ f"Expected str for name, got {type(fetched_name)}"
65
+ )
66
+ self._name = fetched_name
67
+ return self._name
68
+
69
+ @name.setter
70
+ def name(self, value: str) -> None:
71
+ old_name = self.name
72
+ self._name = value
73
+ self._set("name", self._name)
74
+
75
+ logger.debug(f"Renaming light from '{old_name}' to '{value}'")
76
+
77
+ self.bridge.lights_by_name[self.name] = self
78
+ del self.bridge.lights_by_name[old_name]
79
+
80
+ @property
81
+ def on(self) -> bool:
82
+ """Get or set the state of the light [True|False]"""
83
+ self._on = self._get("on")
84
+ assert self._on is not None, "Failed to get 'on' state from bridge."
85
+ assert isinstance(self._on, bool), (
86
+ f"Expected bool for 'on' state, got {type(self._on)}"
87
+ )
88
+ return self._on
89
+
90
+ @on.setter
91
+ def on(self, value: bool) -> None:
92
+ # Some added code here to work around known bug where
93
+ # turning off with transitiontime set makes it restart on brightness = 1
94
+ if self._on and value is False:
95
+ self._reset_bri_after_on = self.transitiontime is not None
96
+ if self._reset_bri_after_on:
97
+ logger.warning(
98
+ "Turned off light with transitiontime specified, brightness will be reset on power on"
99
+ )
100
+
101
+ self._set("on", value)
102
+
103
+ # work around bug by resetting brightness after a power on
104
+ if self._on is False and value is True:
105
+ if isinstance(self._reset_bri_after_on, bool) and self._reset_bri_after_on:
106
+ logger.warning(
107
+ "Light was turned off with transitiontime specified, brightness needs to be reset now."
108
+ )
109
+ if self._brightness is not None:
110
+ self.brightness = self._brightness
111
+ else:
112
+ logger.warning(
113
+ "Cannot reset brightness, initial brightness value not available."
114
+ )
115
+ self._reset_bri_after_on = False
116
+
117
+ self._on = value
118
+
119
+ @property
120
+ def colormode(self) -> str:
121
+ """Get the color mode of the light [hs|xy|ct]"""
122
+ self._colormode = self._get("colormode")
123
+ assert self._colormode is not None, "Failed to get 'colormode' from bridge."
124
+ assert isinstance(self._colormode, str), (
125
+ f"Expected str for colormode, got {type(self._colormode)}"
126
+ )
127
+ return self._colormode
128
+
129
+ @property
130
+ def brightness(self) -> int:
131
+ """Get or set the brightness of the light [0-254].
132
+
133
+ 0 is not off"""
134
+ self._brightness = self._get("bri")
135
+ assert self._brightness is not None, (
136
+ "Failed to get brightness ('bri') from bridge."
137
+ )
138
+ assert isinstance(self._brightness, int), (
139
+ f"Expected int for brightness, got {type(self._brightness)}"
140
+ )
141
+ return self._brightness
142
+
143
+ @brightness.setter
144
+ def brightness(self, value: int) -> None:
145
+ self._brightness = value
146
+ self._set("bri", self._brightness)
147
+
148
+ @property
149
+ def hue(self) -> int:
150
+ """Get or set the hue of the light [0-65535]"""
151
+ self._hue = self._get("hue")
152
+ assert self._hue is not None, "Failed to get hue from bridge."
153
+ assert isinstance(self._hue, int), (
154
+ f"Expected int for hue, got {type(self._hue)}"
155
+ )
156
+ return self._hue
157
+
158
+ @hue.setter
159
+ def hue(self, value: int) -> None:
160
+ self._hue = int(value)
161
+ self._set("hue", self._hue)
162
+
163
+ @property
164
+ def saturation(self) -> int:
165
+ """Get or set the saturation of the light [0-254]
166
+
167
+ 0 = white
168
+ 254 = most saturated
169
+ """
170
+ self._saturation = self._get("sat")
171
+ assert self._saturation is not None, (
172
+ "Failed to get saturation ('sat') from bridge."
173
+ )
174
+ assert isinstance(self._saturation, int), (
175
+ f"Expected int for saturation, got {type(self._saturation)}"
176
+ )
177
+ return self._saturation
178
+
179
+ @saturation.setter
180
+ def saturation(self, value: int) -> None:
181
+ self._saturation = value
182
+ self._set("sat", self._saturation)
183
+
184
+ @property
185
+ def xy(self) -> tuple[float, float]:
186
+ """Get or set the color coordinates of the light [ [0.0-1.0, 0.0-1.0] ]
187
+
188
+ This is in a color space similar to CIE 1931 (but not quite identical)
189
+ """
190
+ self._xy = self._get("xy")
191
+ assert self._xy is not None, (
192
+ "Failed to get color coordinates ('xy') from bridge."
193
+ )
194
+ assert isinstance(self._xy, tuple | list) and len(self._xy) == 2, (
195
+ f"Expected tuple/list of 2 floats for xy, got {self._xy}"
196
+ )
197
+ try:
198
+ xy_tuple = (float(self._xy[0]), float(self._xy[1]))
199
+ self._xy = xy_tuple
200
+ return xy_tuple
201
+ except (ValueError, TypeError, IndexError):
202
+ raise AssertionError(
203
+ f"Could not parse xy coordinates as tuple[float, float]: {self._xy}"
204
+ )
205
+
206
+ @xy.setter
207
+ def xy(self, value: tuple[float, float]) -> None:
208
+ self._xy = value
209
+ self._set("xy", self._xy)
210
+
211
+ @property
212
+ def colortemp(self) -> int:
213
+ """Get or set the color temperature of the light, in units of mireds [154-500]"""
214
+ self._colortemp = self._get("ct")
215
+ assert self._colortemp is not None, (
216
+ "Failed to get color temperature ('ct') from bridge."
217
+ )
218
+ assert isinstance(self._colortemp, int), (
219
+ f"Expected int for color temperature, got {type(self._colortemp)}"
220
+ )
221
+ return self._colortemp
222
+
223
+ @colortemp.setter
224
+ def colortemp(self, value: int) -> None:
225
+ if value < 154:
226
+ logger.warning("154 mireds is coolest allowed color temp")
227
+ elif value > 500:
228
+ logger.warning("500 mireds is warmest allowed color temp")
229
+ self._colortemp = value
230
+ self._set("ct", self._colortemp)
231
+
232
+ @property
233
+ def colortemp_k(self) -> int:
234
+ """Get or set the color temperature of the light, in units of Kelvin [2000-6500]"""
235
+ ct_mired = self.colortemp
236
+ return int(round(1e6 / ct_mired))
237
+
238
+ @colortemp_k.setter
239
+ def colortemp_k(self, value: int) -> None:
240
+ if value > 6500:
241
+ logger.warning("6500 K is max allowed color temp")
242
+ value = 6500
243
+ elif value < 2000:
244
+ logger.warning("2000 K is min allowed color temp")
245
+ value = 2000
246
+
247
+ colortemp_mireds = int(round(1e6 / value))
248
+ logger.debug(f"{value:d} K is {colortemp_mireds} mireds")
249
+ self.colortemp = colortemp_mireds
250
+
251
+ @property
252
+ def effect(self) -> str:
253
+ """Check the effect setting of the light. [none|colorloop]"""
254
+ self._effect = self._get("effect")
255
+ assert self._effect is not None, "Failed to get effect from bridge."
256
+ assert isinstance(self._effect, str), (
257
+ f"Expected str for effect, got {type(self._effect)}"
258
+ )
259
+ return self._effect
260
+
261
+ @effect.setter
262
+ def effect(self, value: str) -> None:
263
+ self._effect = value
264
+ self._set("effect", self._effect)
265
+
266
+ @property
267
+ def alert(self) -> str:
268
+ """Get or set the alert state of the light [select|lselect|none]"""
269
+ self._alert = self._get("alert")
270
+ assert self._alert is not None, "Failed to get alert state from bridge."
271
+ assert isinstance(self._alert, str), (
272
+ f"Expected str for alert state, got {type(self._alert)}"
273
+ )
274
+ return self._alert
275
+
276
+ @alert.setter
277
+ def alert(self, value: str | None) -> None:
278
+ if value is None:
279
+ value = "none"
280
+ self._alert = value
281
+ self._set("alert", self._alert)
282
+
283
+ @property
284
+ def reachable(self) -> bool:
285
+ """Get the reachable state of the light [boolean]"""
286
+ self._reachable = self._get("reachable")
287
+ assert self._reachable is not None, "Failed to get reachable state from bridge."
288
+ assert isinstance(self._reachable, bool), (
289
+ f"Expected bool for reachable state, got {type(self._reachable)}"
290
+ )
291
+ return self._reachable
292
+
293
+ @property
294
+ def type(self) -> str:
295
+ """Get the type of the light [string]"""
296
+ self._type = self._get("type")
297
+ assert self._type is not None, "Failed to get type from bridge."
298
+ assert isinstance(self._type, str), (
299
+ f"Expected str for type, got {type(self._type)}"
300
+ )
301
+ return self._type
phue2/scene.py ADDED
@@ -0,0 +1,58 @@
1
+ """Scene class for Philips Hue scenes."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class Scene:
7
+ """Container for Scene"""
8
+
9
+ def __init__(
10
+ self,
11
+ sid: str,
12
+ appdata: dict[str, Any] | None = None,
13
+ lastupdated: str | None = None,
14
+ lights: list[str] | None = None,
15
+ locked: bool = False,
16
+ name: str = "",
17
+ owner: str = "",
18
+ picture: str = "",
19
+ recycle: bool = False,
20
+ version: int = 0,
21
+ type: str = "",
22
+ group: str = "",
23
+ ):
24
+ """Initialize a Scene object.
25
+
26
+ Args:
27
+ sid: Scene ID
28
+ appdata: Application data associated with the scene
29
+ lastupdated: Last time the scene was updated
30
+ lights: List of light IDs in the scene
31
+ locked: Whether the scene is locked
32
+ name: Name of the scene
33
+ owner: Owner of the scene
34
+ picture: Picture associated with the scene
35
+ recycle: Whether the scene can be recycled
36
+ version: Version of the scene
37
+ type: Type of the scene
38
+ group: Group associated with the scene
39
+ """
40
+ self.scene_id = sid
41
+ self.appdata = appdata or {}
42
+ self.lastupdated = lastupdated
43
+ if lights is not None:
44
+ self.lights = sorted([int(x) for x in lights])
45
+ else:
46
+ self.lights = []
47
+ self.locked = locked
48
+ self.name = name
49
+ self.owner = owner
50
+ self.picture = picture
51
+ self.recycle = recycle
52
+ self.version = version
53
+ self.type = type
54
+ self.group = group
55
+
56
+ def __repr__(self) -> str:
57
+ # like default python repr function, but add scene name
58
+ return f'<{self.__class__.__module__}.{self.__class__.__name__} id="{self.scene_id}" name="{self.name}" lights={self.lights}>'