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/sensor.py ADDED
@@ -0,0 +1,307 @@
1
+ """Sensor classes for interacting with Philips Hue sensors."""
2
+
3
+ import logging
4
+ from collections import UserDict
5
+ from collections.abc import Callable
6
+ from typing import TYPE_CHECKING, Any, TypeAlias
7
+
8
+ if TYPE_CHECKING:
9
+ from phue2 import Bridge
10
+
11
+ logger = logging.getLogger("phue_modern")
12
+
13
+ SensorData: TypeAlias = dict[str, Any]
14
+
15
+
16
+ class _SensorDataWrapper(UserDict[str, Any]):
17
+ """
18
+ Base class for Sensor State/Config wrappers using UserDict.
19
+ Updates the bridge when an item is set.
20
+ Holds a cached view of the state/config.
21
+ """
22
+
23
+ # UserDict stores data in the 'data' attribute (which is a dict)
24
+ data: SensorData
25
+
26
+ def __init__(
27
+ self, bridge: "Bridge", sensor_id: int, initial_data: SensorData | None = None
28
+ ):
29
+ super().__init__(initial_data or {})
30
+ self._bridge = bridge
31
+ self._sensor_id = sensor_id
32
+ # Store the method responsible for updating the bridge (state or config)
33
+ self._update_bridge_method: Callable[[int, SensorData], Any] = (
34
+ self._get_bridge_update_method()
35
+ )
36
+
37
+ def _get_bridge_update_method(self) -> Callable[[int, SensorData], Any]:
38
+ # This method should be implemented by subclasses
39
+ raise NotImplementedError
40
+
41
+ def __setitem__(self, key: str, value: Any) -> None:
42
+ """Sets an item locally and updates the bridge."""
43
+ # Optional: Check if the value is actually changing to avoid unnecessary API calls
44
+ if key in self.data and self.data[key] == value:
45
+ logger.debug(
46
+ f"Skipping bridge update for sensor {self._sensor_id}: key '{key}' value unchanged."
47
+ )
48
+ return
49
+
50
+ # Update local cache *before* calling the bridge
51
+ # If the bridge call fails, the local cache reflects the intended state.
52
+ # Alternatively, update after bridge call succeeds if you want cache
53
+ # to always reflect confirmed bridge state. Let's update before for responsiveness.
54
+ super().__setitem__(key, value)
55
+
56
+ # Update the bridge with *only the changed key/value pair*
57
+ update_payload = {key: value}
58
+ try:
59
+ logger.debug(
60
+ f"Updating bridge for sensor {self._sensor_id}: {update_payload}"
61
+ )
62
+ self._update_bridge_method(self._sensor_id, update_payload)
63
+ except Exception as e:
64
+ # Handle potential API errors (e.g., network issues, invalid key/value)
65
+ logger.error(
66
+ f"Failed to update bridge for sensor {self._sensor_id} with {update_payload}: {e}"
67
+ )
68
+ # Optional: Revert local change if bridge update fails?
69
+ # Depends on desired behavior. For now, keep local change.
70
+ # Consider raising the exception or providing feedback.
71
+ # For simplicity here, we just log the error.
72
+
73
+ def update(self, *args: Any, **kwargs: Any) -> None:
74
+ """
75
+ Updates the dictionary with multiple items.
76
+ Calls the bridge update method for each changed item individually.
77
+ Note: This may result in multiple API calls. If the bridge API supports
78
+ updating multiple keys in one call, this could be optimized.
79
+ """
80
+ # UserDict.update handles merging args and kwargs into self.data
81
+ # We need to intercept this to call the bridge for each change.
82
+ temp_dict = dict(*args, **kwargs)
83
+ for key, value in temp_dict.items():
84
+ # This will call our overridden __setitem__, which handles bridge update
85
+ self[key] = value
86
+
87
+ def sync_from_bridge_data(self, data: SensorData) -> None:
88
+ """
89
+ Updates the local cache directly from data fetched from the bridge,
90
+ bypassing the bridge update mechanism (__setitem__).
91
+ """
92
+ # Use super().update to modify self.data directly without triggering __setitem__
93
+ self.data.clear()
94
+ self.data.update(data)
95
+ # Alternatively: super().update(data) if you want to merge instead of replace.
96
+ # Clearing first ensures the cache exactly matches the fetched data.
97
+
98
+
99
+ class SensorState(_SensorDataWrapper):
100
+ """
101
+ State of a Hue sensor. Updates bridge state on item set.
102
+ Provides a dictionary-like interface to a cached view of the sensor state.
103
+ """
104
+
105
+ def _get_bridge_update_method(self):
106
+ return self._bridge.set_sensor_state
107
+
108
+
109
+ class SensorConfig(_SensorDataWrapper):
110
+ """
111
+ Configuration of a Hue sensor. Updates bridge config on item set.
112
+ Provides a dictionary-like interface to a cached view of the sensor config.
113
+ """
114
+
115
+ def _get_bridge_update_method(self):
116
+ return self._bridge.set_sensor_config
117
+
118
+
119
+ class Sensor:
120
+ """
121
+ Hue Sensor object.
122
+
123
+ Provides access to sensor properties, state, and configuration.
124
+ State and config are cached locally. Modifying `sensor.state['key'] = value`
125
+ or `sensor.config['key'] = value` will update the bridge.
126
+ Call `sensor.refresh()` to update the local cache from the bridge.
127
+ """
128
+
129
+ # Type hint for the bridge reference
130
+ bridge: "Bridge"
131
+ sensor_id: int
132
+
133
+ # Private attributes for caching
134
+ # Use Optional[T] and initialize to None to indicate they haven't been fetched yet.
135
+ _raw_data: SensorData | None = None
136
+ _state: SensorState
137
+ _config: SensorConfig
138
+ # Cache basic attributes to avoid repeated parsing/dict access
139
+ _name: str | None = None
140
+ _modelid: str | None = None
141
+ _swversion: str | None = None
142
+ _type: str | None = None
143
+ _uniqueid: str | None = None
144
+ _manufacturername: str | None = None
145
+ _recycle: bool | None = None
146
+
147
+ def __init__(self, bridge: "Bridge", sensor_id: int):
148
+ self.bridge = bridge
149
+ self.sensor_id = sensor_id
150
+
151
+ # Initialize state and config wrappers without initial data.
152
+ # Data will be populated by refresh() or on first access.
153
+ self._state = SensorState(bridge, sensor_id)
154
+ self._config = SensorConfig(bridge, sensor_id) # Use SensorConfig
155
+
156
+ def __repr__(self) -> str:
157
+ # Fetch name for repr if not already cached. Handle potential errors.
158
+ try:
159
+ name = self.name # Use the property to ensure data is loaded
160
+ except Exception:
161
+ name = f"ID {self.sensor_id}" # Fallback if name fetch fails
162
+ return f'<{self.__class__.__module__}.{self.__class__.__name__} object "{name}" at {hex(id(self))}>'
163
+
164
+ def refresh(self) -> None:
165
+ """Fetches the latest data from the bridge and updates the local cache."""
166
+ logger.debug(f"Refreshing data for sensor {self.sensor_id}")
167
+ try:
168
+ # Assume bridge.get_sensor(id) returns the full sensor dictionary
169
+ self._raw_data = self.bridge.get_sensor(self.sensor_id)
170
+ if not isinstance(self._raw_data, dict):
171
+ raise TypeError(
172
+ f"Expected dict from bridge.get_sensor, got {type(self._raw_data)}"
173
+ )
174
+ except Exception as e:
175
+ logger.error(f"Failed to refresh data for sensor {self.sensor_id}: {e}")
176
+ # Keep stale data or clear it? Let's keep stale data and log error.
177
+ # self._raw_data = None # Option: clear cache on failure
178
+ raise # Re-raise the exception so the caller knows refresh failed
179
+
180
+ # Update cached attributes directly from raw data
181
+ self._name = self._raw_data.get("name")
182
+ self._modelid = self._raw_data.get("modelid")
183
+ self._swversion = self._raw_data.get("swversion")
184
+ self._type = self._raw_data.get("type")
185
+ self._uniqueid = self._raw_data.get("uniqueid")
186
+ self._manufacturername = self._raw_data.get("manufacturername")
187
+ self._recycle = self._raw_data.get("recycle")
188
+
189
+ # Update state and config caches using the sync method (avoids bridge writes)
190
+ self._state.sync_from_bridge_data(self._raw_data.get("state", {}))
191
+ self._config.sync_from_bridge_data(self._raw_data.get("config", {}))
192
+ logger.debug(f"Sensor {self.sensor_id} data refreshed successfully.")
193
+
194
+ def _ensure_data(self) -> None:
195
+ """Ensures sensor data has been fetched at least once, calling refresh() if needed."""
196
+ if self._raw_data is None:
197
+ logger.debug(f"Initial data fetch for sensor {self.sensor_id}")
198
+ self.refresh() # This will fetch and populate everything
199
+
200
+ # --- Properties ---
201
+
202
+ @property
203
+ def name(self) -> str:
204
+ """Get or set the name of the sensor [string]. Fetches data on first access."""
205
+ self._ensure_data()
206
+ # Provide a default/fallback if name is somehow missing after refresh
207
+ return self._name or f"Sensor {self.sensor_id}"
208
+
209
+ @name.setter
210
+ def name(self, value: str) -> None:
211
+ if not isinstance(value, str): # type: ignore[unnecessary-isinstance]
212
+ raise TypeError("Sensor name must be a string")
213
+
214
+ # Get the current name (ensures data is loaded if it wasn't already)
215
+ old_name = self.name
216
+ if old_name == value:
217
+ return # No change needed
218
+
219
+ logger.debug(
220
+ f"Attempting to rename sensor {self.sensor_id} from '{old_name}' to '{value}'"
221
+ )
222
+ try:
223
+ # Use bridge.set_sensor for top-level attributes like name
224
+ self.bridge.set_sensor(self.sensor_id, {"name": value})
225
+ except Exception as e:
226
+ logger.error(
227
+ f"Failed to set name for sensor {self.sensor_id} via bridge: {e}"
228
+ )
229
+ raise # Re-raise exception
230
+
231
+ # Update local cache only after successful bridge call
232
+ self._name = value
233
+ if self._raw_data: # Update raw data cache if it exists
234
+ self._raw_data["name"] = value
235
+
236
+ # Update bridge's name lookup cache (if it exists and is used)
237
+ if hasattr(self.bridge, "sensors_by_name"):
238
+ # Be careful modifying another object's state directly
239
+ # Ensure this is the intended interaction pattern with the Bridge class
240
+ if old_name in self.bridge.sensors_by_name:
241
+ del self.bridge.sensors_by_name[old_name]
242
+ self.bridge.sensors_by_name[value] = self
243
+ logger.info(f"Sensor {self.sensor_id} renamed to '{value}'")
244
+
245
+ # Read-only properties using the cache
246
+ @property
247
+ def modelid(self) -> str | None:
248
+ """Get hardware model ID [string, read-only]. Fetches data on first access."""
249
+ self._ensure_data()
250
+ return self._modelid
251
+
252
+ @property
253
+ def swversion(self) -> str | None:
254
+ """Get firmware version [string, read-only]. Fetches data on first access."""
255
+ self._ensure_data()
256
+ return self._swversion
257
+
258
+ @property
259
+ def type(self) -> str | None:
260
+ """Get sensor type [string, read-only]. Fetches data on first access."""
261
+ self._ensure_data()
262
+ return self._type
263
+
264
+ @property
265
+ def uniqueid(self) -> str | None:
266
+ """Get unique device ID [string, read-only]. Fetches data on first access."""
267
+ self._ensure_data()
268
+ return self._uniqueid
269
+
270
+ @property
271
+ def manufacturername(self) -> str | None:
272
+ """Get manufacturer name [string, read-only]. Fetches data on first access."""
273
+ self._ensure_data()
274
+ return self._manufacturername
275
+
276
+ @property
277
+ def recycle(self) -> bool | None:
278
+ """Check if resource should be auto-removed [bool, read-only]. Fetches data on first access."""
279
+ self._ensure_data()
280
+ return self._recycle
281
+
282
+ # State and Config properties return the cached wrapper objects
283
+ @property
284
+ def state(self) -> SensorState:
285
+ """
286
+ Get the cached state of the sensor [SensorState].
287
+ Modifying items (e.g., `sensor.state['on'] = True`) updates the bridge.
288
+ Call `sensor.refresh()` to sync cache from the bridge.
289
+ Fetches data on first access.
290
+ """
291
+ self._ensure_data()
292
+ return self._state
293
+
294
+ # Removed state.setter - modifications happen via the returned object
295
+
296
+ @property
297
+ def config(self) -> SensorConfig:
298
+ """
299
+ Get the cached config of the sensor [SensorConfig].
300
+ Modifying items (e.g., `sensor.config['on'] = True`) updates the bridge.
301
+ Call `sensor.refresh()` to sync cache from the bridge.
302
+ Fetches data on first access.
303
+ """
304
+ self._ensure_data()
305
+ return self._config
306
+
307
+ # Removed config.setter - modifications happen via the returned object
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: phue2
3
+ Version: 0.0.1.dev279
4
+ Summary: A Philips Hue Python library
5
+ Author: Nathanaël Lécaudé, Nathan Nowack
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx
10
+ Description-Content-Type: text/markdown
11
+
12
+ # phue2
13
+
14
+ modern Python library to control the Philips Hue lighting system
15
+
16
+ This is a fork of the original [phue library](https://github.com/studioimaginaire/phue) by Nathanaël Lécaudé, modernized with type annotations, improved error handling, and a fully-featured CLI. The library remains MIT licensed.
17
+
18
+ > [!IMPORTANT]
19
+
20
+ > I appreciate the original authors work and if there's any interest in merging these (substantial) changes back into the original library, I'm happy to do so.
21
+
22
+ ## Installation
23
+
24
+ ### Using uv
25
+
26
+ ```bash
27
+ uv add phue2
28
+
29
+ uv pip install phue2
30
+ ```
31
+
32
+ ### Using pip
33
+
34
+ ```bash
35
+ pip install phue2
36
+ ```
37
+
38
+ ## Repository Structure
39
+
40
+ ```
41
+ .
42
+ ├── LICENSE
43
+ ├── README.md
44
+ ├── examples/
45
+ ├── src/
46
+ │ └── phue/
47
+ │ ├── __init__.py
48
+ │ ├── __main__.py # CLI interface
49
+ │ ├── bridge.py # Bridge connection handling
50
+ │ ├── exceptions.py # Custom exceptions
51
+ │ ├── group.py # Group controls
52
+ │ ├── light.py # Light controls
53
+ │ ├── scene.py # Scene handling
54
+ │ └── sensor.py # Sensor controls
55
+ └── tests/ # Tests
56
+ ```
57
+
58
+ ## Requirements
59
+
60
+ - Python 3.10 or higher (not compatible with Python 2.x)
61
+ - httpx (for network requests)
62
+
63
+ ## Features
64
+
65
+ - Fully typed with Python type annotations
66
+ - Robust error handling with custom exceptions
67
+ - Comprehensive test suite
68
+ - Colorful, user-friendly command-line interface
69
+ - Support for Lights, Groups, Scenes, and Sensors
70
+ - Auto-discovery of Hue bridges on the network
71
+ - Simple and intuitive API for controlling Hue devices
72
+
73
+
74
+
75
+ ## Command Line Usage
76
+
77
+ The library includes a command-line interface for controlling your Hue lights:
78
+
79
+ ```bash
80
+ # List all lights
81
+ phue ls
82
+
83
+ # Get details about a specific light
84
+ phue get light "Living Room"
85
+
86
+ # Turn on a light and set brightness
87
+ phue set light "Kitchen" --on --bri 200
88
+
89
+ # List all groups
90
+ phue ls groups
91
+
92
+ # Turn off lights in a group
93
+ phue set group "Downstairs" --off
94
+ ```
95
+
96
+ ## Basic Usage
97
+
98
+ Using the set_light and get_light methods you can control pretty much all the parameters:
99
+
100
+ ```python
101
+ from phue2 import Bridge
102
+
103
+ # Connect to the bridge
104
+ b = Bridge('192.168.1.100')
105
+
106
+ # If the app is not registered and the button is not pressed, press the button and call connect()
107
+ # This only needs to be run a single time
108
+ b.connect()
109
+
110
+ # Get the bridge state (This returns the full dictionary that you can explore)
111
+ b.get_api()
112
+
113
+ # Prints if light 1 is on or not
114
+ b.get_light(1, 'on')
115
+
116
+ # Set brightness of lamp 1 to max
117
+ b.set_light(1, 'bri', 254)
118
+
119
+ # Turn lamp 2 on
120
+ b.set_light(2, 'on', True)
121
+
122
+ # You can also control multiple lamps by sending a list as lamp_id
123
+ b.set_light([1, 2], 'on', True)
124
+
125
+ # You can also use light names instead of the id
126
+ b.get_light('Kitchen')
127
+ b.set_light('Kitchen', 'bri', 254)
128
+
129
+ # Also works with lists
130
+ b.set_light(['Bathroom', 'Garage'], 'on', False)
131
+ ```
132
+
133
+ ### Light Objects
134
+
135
+ If you want to work in a more object-oriented way, you can get Light objects:
136
+
137
+ ```python
138
+ # Get a flat list of light objects
139
+ lights = b.lights
140
+
141
+ # Print light names
142
+ for light in lights:
143
+ print(light.name)
144
+
145
+ # Set brightness of each light to 127
146
+ for light in lights:
147
+ light.brightness = 127
148
+
149
+ # Get a dictionary with the light name as the key
150
+ light_names = b.get_light_objects('name')
151
+
152
+ # Set lights using name as key
153
+ for light_name in ['Kitchen', 'Bedroom', 'Garage']:
154
+ light = light_names.get(light_name)
155
+ if light:
156
+ light.on = True
157
+ light.hue = 15000
158
+ light.saturation = 120
159
+ ```
160
+
161
+ ## Error Handling
162
+
163
+ The library provides custom exceptions for better error handling:
164
+
165
+ ```python
166
+ from phue2 import Bridge, PhueRegistrationException, PhueRequestTimeout
167
+
168
+ try:
169
+ b = Bridge('192.168.1.100')
170
+ b.connect()
171
+ except PhueRegistrationException:
172
+ print("Press the button on the bridge and try again")
173
+ except PhueRequestTimeout:
174
+ print("Could not connect to the bridge - check your network")
175
+ ```
176
+
177
+ ## Acknowledgments
178
+
179
+ This project is a fork of the original [phue](https://github.com/studioimaginaire/phue) library created by Nathanaël Lécaudé.
180
+
181
+ The modernized version was created by [zzstoatzz](https://github.com/zzstoatzz) to add type annotations and a more opinionated CLI.
182
+
183
+ ## License
184
+
185
+ MIT - http://opensource.org/licenses/MIT
186
+
187
+ "Hue Personal Wireless Lighting" is a trademark owned by Koninklijke Philips Electronics N.V., see www.meethue.com for more information.
188
+ I am in no way affiliated with the Philips organization.
@@ -0,0 +1,15 @@
1
+ phue2/__init__.py,sha256=Uu9QwkzgpJtpWpYEO7MWuAVRPrHclQ7NkbrzKIIyFeU,873
2
+ phue2/__main__.py,sha256=22DGc0YJGxtF5eOQlnYhH29hPLkvh56svKONyqnrTEQ,12987
3
+ phue2/bridge.py,sha256=iFv0pq19bJ3r8El3ZwOzrfac-pls9d8y1PMbxv7cbBg,43862
4
+ phue2/exceptions.py,sha256=tFN3uEkHx2k-oU-wnmg1oKeoZJRrtZSS7L1iep5NHJ8,540
5
+ phue2/group.py,sha256=0ttIZrN-wJPfyXCv9CFxdsJuiYqhiy44gjojeIUI-5A,3995
6
+ phue2/light.py,sha256=BfvsiXUszEHh-1s1n3Nn-cTjiIPpBQ-JNiM-hAqpd5s,10793
7
+ phue2/scene.py,sha256=SB2BTVVS01ytPPvIp8njeXFYCKYi0cZLmxxBdplYKZo,1827
8
+ phue2/sensor.py,sha256=iSe0goDK-xX2AVUJh5Gi60MhKVaylLyMW16l7oEmEpY,12431
9
+ phue2/_internal/__init__.py,sha256=E6FcITKZNPECEK0c0umWMezKBVY6OCDqJpbOkbBfVNw,38
10
+ phue2/_internal/console.py,sha256=nUHNNLUhCAaaBLi82c-gJJXjSFScpFPgZWinxx3JYcs,4853
11
+ phue2-0.0.1.dev279.dist-info/METADATA,sha256=S_QMhD3uo0QqTomtr0NTD21OCIW3cQFhcGiAlireibs,4749
12
+ phue2-0.0.1.dev279.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ phue2-0.0.1.dev279.dist-info/entry_points.txt,sha256=izaWBWq5BqZKkEzL_Cha5PflzTST2vAmGd9NFm-3rk4,45
14
+ phue2-0.0.1.dev279.dist-info/licenses/LICENSE,sha256=53NTqbgURXQ5wulynQxi47i8YBuEcfThsunG4LSJs4s,1080
15
+ phue2-0.0.1.dev279.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ phue = phue2.__main__:main
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Nathan Nowack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.