phue2 0.0.1.dev280__tar.gz → 0.0.4__tar.gz
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-0.0.4/.cursor/rules/docs.mdc +14 -0
- phue2-0.0.4/.cursor/rules/python-style.mdc +36 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/.github/workflows/lint.yml +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/.github/workflows/publish.yml +1 -1
- phue2-0.0.4/CHANGELOG.md +64 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/LICENSE +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/PKG-INFO +7 -7
- {phue2-0.0.1.dev280 → phue2-0.0.4}/README.md +6 -6
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/connect_bridge.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/flicker.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/hue-rainbow.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/random_colors.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/repros/smart_home.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/rgb_colors.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/sine_wave_lights.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/tk_gui_complex.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/tk_gui_hsb.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/examples/tk_gui_simple.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/pyproject.toml +4 -10
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/__init__.py +2 -2
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/__main__.py +50 -73
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/bridge.py +15 -11
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/group.py +2 -2
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/light.py +1 -1
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/sensor.py +1 -1
- phue2-0.0.4/tests/test_basic_import.py +7 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/tests/test_bridge.py +34 -7
- {phue2-0.0.1.dev280 → phue2-0.0.4}/tests/test_cli.py +17 -39
- {phue2-0.0.1.dev280 → phue2-0.0.4}/tests/test_request.py +14 -14
- {phue2-0.0.1.dev280 → phue2-0.0.4}/tests/test_scene.py +1 -1
- {phue2-0.0.1.dev280 → phue2-0.0.4}/uv.lock +1 -1
- phue2-0.0.1.dev280/tests/test_basic_import.py +0 -7
- {phue2-0.0.1.dev280 → phue2-0.0.4}/.github/workflows/test.yml +0 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/.gitignore +0 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/.pre-commit-config.yaml +0 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/justfile +0 -0
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/_internal/__init__.py +0 -0
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/_internal/console.py +0 -0
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/exceptions.py +0 -0
- {phue2-0.0.1.dev280/src/phue2 → phue2-0.0.4/src/phue}/scene.py +0 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/tests/__init__.py +0 -0
- {phue2-0.0.1.dev280 → phue2-0.0.4}/tests/samples.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: style and tone for documentation
|
|
3
|
+
globs: *.mdx
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# tone
|
|
8
|
+
- casual, clear and concise
|
|
9
|
+
- use "you" to refer to the user (e.g. "Foo is great when you want Bar")
|
|
10
|
+
|
|
11
|
+
# content
|
|
12
|
+
- breathable
|
|
13
|
+
- makes good use of admonitions and mintlify's features like <CodeGroup> and <Tip>
|
|
14
|
+
- interspersed with practical and simple examples with output in <Accordian> where apt
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: how to write, test and run python
|
|
3
|
+
globs: *.py
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Python Style
|
|
8
|
+
|
|
9
|
+
## Design
|
|
10
|
+
- aggressively minimal and elegant
|
|
11
|
+
- prefer functional over OOP
|
|
12
|
+
- keep implementation details "private" (e.g. def _impl)
|
|
13
|
+
|
|
14
|
+
## Type Hints
|
|
15
|
+
- Marvin supports Python 3.10+
|
|
16
|
+
- Use modern syntax and full type annotations
|
|
17
|
+
- Use | instead of Union
|
|
18
|
+
- Use builtins like list, dict instead of typing.List and typing.Dict
|
|
19
|
+
- use | None instead of Optional
|
|
20
|
+
- Always type optional keyword arguments correctly e.g. def f(x: int | None = None)
|
|
21
|
+
|
|
22
|
+
## Running Commands
|
|
23
|
+
`uv` gives us superpowers to resolve deps fast and on the fly. no more pip or fussing with venvs
|
|
24
|
+
|
|
25
|
+
### Install dependencies
|
|
26
|
+
- install project deps: `uv sync`
|
|
27
|
+
- install project deps with an extra: `uv sync --extra foo`
|
|
28
|
+
|
|
29
|
+
### Running script
|
|
30
|
+
- run script with existing project deps: `uv run some/script.py`
|
|
31
|
+
- run script additional deps on the fly: `uv run --with pandas script.py`
|
|
32
|
+
|
|
33
|
+
### Testing
|
|
34
|
+
- test all: `uv run pytest`
|
|
35
|
+
- test all with 3 runners: `uv run pytest -n3`
|
|
36
|
+
- test certain folder, only matching cases: `uv run pytest tests/basic -k some_test_fn_subtr`
|
phue2-0.0.4/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# phue changelog
|
|
2
|
+
|
|
3
|
+
## r12
|
|
4
|
+
- split functionality into modules
|
|
5
|
+
- added `httpx` dependency
|
|
6
|
+
- dropped support for python 2 and python <3.10
|
|
7
|
+
- updated packaging and tooling
|
|
8
|
+
- added complete type hints and lint checks
|
|
9
|
+
|
|
10
|
+
## r11
|
|
11
|
+
- Add support for deleting scenes
|
|
12
|
+
- Various bug fixes
|
|
13
|
+
|
|
14
|
+
## r10
|
|
15
|
+
- Misc bug fixes
|
|
16
|
+
- Better support for schedules
|
|
17
|
+
|
|
18
|
+
## r9
|
|
19
|
+
- Added unit tests (sdague)
|
|
20
|
+
- Added scene support (sdague)
|
|
21
|
+
- Added sensor support (eldstal)
|
|
22
|
+
- Added reachable and type attributes to the Light object (carlosperate)
|
|
23
|
+
- Changed License to MIT
|
|
24
|
+
|
|
25
|
+
## r8
|
|
26
|
+
- iOS compatibility (Nathanaël Lécaudé)
|
|
27
|
+
- Logging fixes
|
|
28
|
+
- Added effect changing options (bradykent)
|
|
29
|
+
- Several unicode fixes (Nathanaël Lécaudé)
|
|
30
|
+
- Misc bug fixes
|
|
31
|
+
|
|
32
|
+
## r7
|
|
33
|
+
- Added to pypi
|
|
34
|
+
- Added support for Python 3 (Nathanaël Lécaudé)
|
|
35
|
+
- Logging level can be set with b.set_logging() (Nathanaël Lécaudé)
|
|
36
|
+
- Logging level can be set at init: b = Bridge(logging = 'debug') (Nathanaël Lécaudé)
|
|
37
|
+
- Added docstrings to Light properties (Nathanaël Lécaudé)
|
|
38
|
+
- Added colormode property to Light class (Nathanaël Lécaudé)
|
|
39
|
+
- IP is now optional if present in config file (Nathanaël Lécaudé)
|
|
40
|
+
- Implemented groups (Nathanaël Lécaudé)
|
|
41
|
+
- Implemented schedules (Nathanaël Lécaudé)
|
|
42
|
+
- Renamed get_info to get_api (Nathanaël Lécaudé)
|
|
43
|
+
- Renamed get_lights to get_light_objects (Nathanaël Lécaudé)
|
|
44
|
+
- Renamed set_state and get_state to set_light and get_light (Nathanaël Lécaudé)
|
|
45
|
+
- Fixed important bug when using set_state with a list of lights (Nathanaël Lécaudé)
|
|
46
|
+
- Add access to Light objects via direct indexing of the Bridge object via __getitem__ (Marshall Perrin)
|
|
47
|
+
- Implement real logging using Python's logging module, including error checking and display of responses from the server. (Marshall Perrin)
|
|
48
|
+
- Add function colortemp_k for color temperatures in Kelvin. (Marshall Perrin)
|
|
49
|
+
- Some additional error checking for invalid or missing parameters (Marshall Perrin)
|
|
50
|
+
- More details in docstrings. (Marshall Perrin)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## r6
|
|
54
|
+
- Light objects are now obtained using the get_lights method
|
|
55
|
+
- Added the alert method to the Light object
|
|
56
|
+
- All requests now use httplib for consistency
|
|
57
|
+
- Moved all source to github
|
|
58
|
+
- Renamed the module to phue
|
|
59
|
+
|
|
60
|
+
## r5
|
|
61
|
+
- Renamed the Bulb() object to Light() so it reflects the official API better
|
|
62
|
+
- You can now pass the username as argument to the Bridge class if you don't want to read/store to file
|
|
63
|
+
- You can now get the bridge name with brdige.name or set it with bridge.name = 'newname'
|
|
64
|
+
- The set_state method can now use a dictionary as first argument to send more complex messages
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025 Nathan Nowack
|
|
3
|
+
Copyright (c) 2025 Nathan Nowack and Nathanaël Lécaudé
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phue2
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A Philips Hue Python library
|
|
5
5
|
Author: Nathanaël Lécaudé, Nathan Nowack
|
|
6
6
|
License: MIT
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.10
|
|
|
9
9
|
Requires-Dist: httpx
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# phue
|
|
13
13
|
|
|
14
14
|
modern Python library to control the Philips Hue lighting system
|
|
15
15
|
|
|
@@ -24,15 +24,15 @@ This is a fork of the original [phue library](https://github.com/studioimaginair
|
|
|
24
24
|
### Using uv
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
uv add
|
|
27
|
+
uv add phue
|
|
28
28
|
|
|
29
|
-
uv pip install
|
|
29
|
+
uv pip install phue
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
### Using pip
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
pip install
|
|
35
|
+
pip install phue
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Repository Structure
|
|
@@ -98,7 +98,7 @@ phue set group "Downstairs" --off
|
|
|
98
98
|
Using the set_light and get_light methods you can control pretty much all the parameters:
|
|
99
99
|
|
|
100
100
|
```python
|
|
101
|
-
from
|
|
101
|
+
from phue import Bridge
|
|
102
102
|
|
|
103
103
|
# Connect to the bridge
|
|
104
104
|
b = Bridge('192.168.1.100')
|
|
@@ -163,7 +163,7 @@ for light_name in ['Kitchen', 'Bedroom', 'Garage']:
|
|
|
163
163
|
The library provides custom exceptions for better error handling:
|
|
164
164
|
|
|
165
165
|
```python
|
|
166
|
-
from
|
|
166
|
+
from phue import Bridge, PhueRegistrationException, PhueRequestTimeout
|
|
167
167
|
|
|
168
168
|
try:
|
|
169
169
|
b = Bridge('192.168.1.100')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# phue
|
|
2
2
|
|
|
3
3
|
modern Python library to control the Philips Hue lighting system
|
|
4
4
|
|
|
@@ -13,15 +13,15 @@ This is a fork of the original [phue library](https://github.com/studioimaginair
|
|
|
13
13
|
### Using uv
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
uv add
|
|
16
|
+
uv add phue
|
|
17
17
|
|
|
18
|
-
uv pip install
|
|
18
|
+
uv pip install phue
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
### Using pip
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
pip install
|
|
24
|
+
pip install phue
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Repository Structure
|
|
@@ -87,7 +87,7 @@ phue set group "Downstairs" --off
|
|
|
87
87
|
Using the set_light and get_light methods you can control pretty much all the parameters:
|
|
88
88
|
|
|
89
89
|
```python
|
|
90
|
-
from
|
|
90
|
+
from phue import Bridge
|
|
91
91
|
|
|
92
92
|
# Connect to the bridge
|
|
93
93
|
b = Bridge('192.168.1.100')
|
|
@@ -152,7 +152,7 @@ for light_name in ['Kitchen', 'Bedroom', 'Garage']:
|
|
|
152
152
|
The library provides custom exceptions for better error handling:
|
|
153
153
|
|
|
154
154
|
```python
|
|
155
|
-
from
|
|
155
|
+
from phue import Bridge, PhueRegistrationException, PhueRequestTimeout
|
|
156
156
|
|
|
157
157
|
try:
|
|
158
158
|
b = Bridge('192.168.1.100')
|
|
@@ -9,7 +9,7 @@ import logging
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from phue import Bridge, Light, PhueRegistrationException, console
|
|
13
13
|
|
|
14
14
|
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@ This script demonstrates how to set the color of a light to a specific RGB color
|
|
|
4
4
|
WARNING: If you have not previously connected to the bridge, run connect_bridge.py first.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from phue import Bridge
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def rgb_to_xy(red: float, green: float, blue: float) -> list[float]:
|
|
@@ -9,7 +9,7 @@ readme = "README.md"
|
|
|
9
9
|
license = { text = "MIT" }
|
|
10
10
|
|
|
11
11
|
[project.scripts]
|
|
12
|
-
phue = "
|
|
12
|
+
phue = "phue.__main__:main"
|
|
13
13
|
|
|
14
14
|
[dependency-groups]
|
|
15
15
|
dev = [
|
|
@@ -28,20 +28,14 @@ dev = [
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
[build-system]
|
|
31
|
-
requires = ["hatchling", "
|
|
31
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
32
32
|
build-backend = "hatchling.build"
|
|
33
33
|
|
|
34
34
|
[tool.hatch.build.targets.wheel]
|
|
35
|
-
packages = ["src/
|
|
35
|
+
packages = ["src/phue"]
|
|
36
36
|
|
|
37
37
|
[tool.hatch.version]
|
|
38
|
-
source = "
|
|
39
|
-
|
|
40
|
-
[tool.uv-dynamic-versioning]
|
|
41
|
-
vcs = "git"
|
|
42
|
-
style = "pep440"
|
|
43
|
-
bump = true
|
|
44
|
-
metadata = false
|
|
38
|
+
source = "vcs"
|
|
45
39
|
|
|
46
40
|
|
|
47
41
|
[tool.pytest.ini_options]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
phue - A modernized Philips Hue Python library
|
|
3
3
|
Based on the original phue by Nathanaël Lécaudé
|
|
4
4
|
Original protocol hacking by rsmck: http://rsmck.co.uk/hue
|
|
5
5
|
|
|
@@ -18,7 +18,7 @@ from ._internal.console import console
|
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
20
|
|
|
21
|
-
logger = logging.getLogger("
|
|
21
|
+
logger = logging.getLogger("phue")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
@@ -6,8 +6,8 @@ import logging
|
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
9
|
+
from phue import Bridge, PhueRegistrationException
|
|
10
|
+
from phue._internal.console import (
|
|
11
11
|
BLUE,
|
|
12
12
|
BOLD,
|
|
13
13
|
CYAN,
|
|
@@ -60,6 +60,12 @@ def parse_args(
|
|
|
60
60
|
)
|
|
61
61
|
parser.add_argument("--config-file-path", help="Path to the config file")
|
|
62
62
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--no-save-config",
|
|
65
|
+
action="store_false",
|
|
66
|
+
dest="save_config",
|
|
67
|
+
help="Do not save connection details to the config file",
|
|
68
|
+
)
|
|
63
69
|
|
|
64
70
|
# Command subparsers
|
|
65
71
|
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
@@ -161,7 +167,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
161
167
|
console.info(f"Connecting to bridge at {args.host}...")
|
|
162
168
|
while True:
|
|
163
169
|
try:
|
|
164
|
-
bridge = Bridge(
|
|
170
|
+
bridge = Bridge(
|
|
171
|
+
args.host,
|
|
172
|
+
config_file_path=args.config_file_path,
|
|
173
|
+
save_config=args.save_config,
|
|
174
|
+
)
|
|
165
175
|
console.success("Successfully connected to the bridge!")
|
|
166
176
|
break
|
|
167
177
|
except PhueRegistrationException:
|
|
@@ -243,34 +253,30 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
243
253
|
name = args.name
|
|
244
254
|
|
|
245
255
|
if resource == "light":
|
|
246
|
-
# Try by ID first, then by name
|
|
247
|
-
light = None
|
|
248
256
|
try:
|
|
249
|
-
|
|
250
|
-
light
|
|
251
|
-
except
|
|
252
|
-
pass
|
|
253
|
-
|
|
254
|
-
if not light:
|
|
255
|
-
light = bridge.lights_by_name.get(name)
|
|
256
|
-
|
|
257
|
-
if not light:
|
|
257
|
+
light = bridge.get_light(name)
|
|
258
|
+
assert light["name"]
|
|
259
|
+
except KeyError:
|
|
258
260
|
console.error(f"Light '{name}' not found")
|
|
259
261
|
return 1
|
|
260
262
|
|
|
261
|
-
console.info(styled_for_cli(f"LIGHT: {light
|
|
263
|
+
console.info(styled_for_cli(f"LIGHT: {light['name']}", YELLOW + BOLD))
|
|
264
|
+
console.info(
|
|
265
|
+
f" {styled_for_cli('Status:', BLUE)} {styled_for_cli('ON' if light['state']['on'] else 'OFF', GREEN if light['state']['on'] else RED)}"
|
|
266
|
+
)
|
|
267
|
+
console.info(f" {styled_for_cli('Type:', BLUE)} {light['type']}")
|
|
262
268
|
console.info(
|
|
263
|
-
f" {styled_for_cli('
|
|
269
|
+
f" {styled_for_cli('Brightness:', BLUE)} {light['state']['bri']}/254"
|
|
264
270
|
)
|
|
265
|
-
console.info(f" {styled_for_cli('Type:', BLUE)} {light.type}")
|
|
266
271
|
console.info(
|
|
267
|
-
f" {styled_for_cli('
|
|
272
|
+
f" {styled_for_cli('Hue:', BLUE)} {light['state']['hue']}/65535"
|
|
268
273
|
)
|
|
269
|
-
console.info(f" {styled_for_cli('Hue:', BLUE)} {light.hue}/65535")
|
|
270
274
|
console.info(
|
|
271
|
-
f" {styled_for_cli('Saturation:', BLUE)} {light
|
|
275
|
+
f" {styled_for_cli('Saturation:', BLUE)} {light['state']['sat']}/254"
|
|
276
|
+
)
|
|
277
|
+
console.info(
|
|
278
|
+
f" {styled_for_cli('Reachable:', BLUE)} {light['state']['reachable']}"
|
|
272
279
|
)
|
|
273
|
-
console.info(f" {styled_for_cli('Reachable:', BLUE)} {light.reachable}")
|
|
274
280
|
|
|
275
281
|
elif resource == "group":
|
|
276
282
|
# Try to find group by name
|
|
@@ -312,45 +318,39 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
312
318
|
resource = args.resource
|
|
313
319
|
name = args.name
|
|
314
320
|
changed = False
|
|
321
|
+
state_changes: dict[str, object] = {}
|
|
322
|
+
|
|
323
|
+
if args.on:
|
|
324
|
+
state_changes["on"] = True
|
|
325
|
+
changed = True
|
|
326
|
+
elif args.off:
|
|
327
|
+
state_changes["on"] = False
|
|
328
|
+
changed = True
|
|
329
|
+
|
|
330
|
+
if args.bri is not None:
|
|
331
|
+
state_changes["bri"] = args.bri
|
|
332
|
+
changed = True
|
|
333
|
+
|
|
334
|
+
if args.hue is not None:
|
|
335
|
+
state_changes["hue"] = args.hue
|
|
336
|
+
changed = True
|
|
337
|
+
|
|
338
|
+
if args.sat is not None:
|
|
339
|
+
state_changes["sat"] = args.sat
|
|
340
|
+
changed = True
|
|
315
341
|
|
|
316
342
|
if resource == "light":
|
|
317
|
-
# Try by ID first, then by name
|
|
318
343
|
light = None
|
|
319
|
-
try:
|
|
320
|
-
light_id = int(name)
|
|
321
|
-
light = bridge.lights_by_id.get(light_id)
|
|
322
|
-
except ValueError:
|
|
323
|
-
pass
|
|
324
|
-
|
|
325
344
|
if not light:
|
|
326
|
-
light = bridge.
|
|
345
|
+
light = bridge.get_light(name)
|
|
327
346
|
|
|
328
347
|
if not light:
|
|
329
348
|
console.error(f"Light '{name}' not found")
|
|
330
349
|
return 1
|
|
331
350
|
|
|
332
|
-
# Apply state changes
|
|
333
|
-
if args.on:
|
|
334
|
-
light.on = True
|
|
335
|
-
changed = True
|
|
336
|
-
elif args.off:
|
|
337
|
-
light.on = False
|
|
338
|
-
changed = True
|
|
339
|
-
|
|
340
|
-
if args.bri is not None:
|
|
341
|
-
light.brightness = args.bri
|
|
342
|
-
changed = True
|
|
343
|
-
|
|
344
|
-
if args.hue is not None:
|
|
345
|
-
light.hue = args.hue
|
|
346
|
-
changed = True
|
|
347
|
-
|
|
348
|
-
if args.sat is not None:
|
|
349
|
-
light.saturation = args.sat
|
|
350
|
-
changed = True
|
|
351
|
-
|
|
352
351
|
if changed:
|
|
353
|
-
|
|
352
|
+
bridge.set_light(name, state_changes)
|
|
353
|
+
console.success(f"Updated light '{light['name']}'")
|
|
354
354
|
else:
|
|
355
355
|
console.warning("No changes specified")
|
|
356
356
|
|
|
@@ -372,29 +372,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
372
372
|
console.error(f"Group '{name}' not found")
|
|
373
373
|
return 1
|
|
374
374
|
|
|
375
|
-
# Apply state changes
|
|
376
|
-
state_changes: dict[str, object] = {}
|
|
377
|
-
changed = False
|
|
378
|
-
|
|
379
|
-
if args.on:
|
|
380
|
-
state_changes["on"] = True
|
|
381
|
-
changed = True
|
|
382
|
-
elif args.off:
|
|
383
|
-
state_changes["on"] = False
|
|
384
|
-
changed = True
|
|
385
|
-
|
|
386
|
-
if args.bri is not None:
|
|
387
|
-
state_changes["bri"] = args.bri
|
|
388
|
-
changed = True
|
|
389
|
-
|
|
390
|
-
if args.hue is not None:
|
|
391
|
-
state_changes["hue"] = args.hue
|
|
392
|
-
changed = True
|
|
393
|
-
|
|
394
|
-
if args.sat is not None:
|
|
395
|
-
state_changes["sat"] = args.sat
|
|
396
|
-
changed = True
|
|
397
|
-
|
|
398
375
|
if changed:
|
|
399
376
|
bridge.set_group(group_id, state_changes)
|
|
400
377
|
console.success(f"Updated group {name}")
|
|
@@ -9,15 +9,15 @@ from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from phue.exceptions import (
|
|
13
13
|
PhueException,
|
|
14
14
|
PhueRegistrationException,
|
|
15
15
|
PhueRequestTimeout,
|
|
16
16
|
)
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
17
|
+
from phue.group import Group
|
|
18
|
+
from phue.light import Light
|
|
19
|
+
from phue.scene import Scene
|
|
20
|
+
from phue.sensor import Sensor
|
|
21
21
|
|
|
22
22
|
logger = logging.getLogger("phue_modern")
|
|
23
23
|
|
|
@@ -46,6 +46,7 @@ class Bridge:
|
|
|
46
46
|
username: str | None = None,
|
|
47
47
|
config_file_path: str | None = None,
|
|
48
48
|
timeout: int = 10,
|
|
49
|
+
save_config: bool = True,
|
|
49
50
|
):
|
|
50
51
|
"""Initialization function.
|
|
51
52
|
|
|
@@ -54,6 +55,7 @@ class Bridge:
|
|
|
54
55
|
username: Optional username for the bridge
|
|
55
56
|
config_file_path: Optional path to the configuration file
|
|
56
57
|
timeout: Request timeout in seconds (default: 10)
|
|
58
|
+
save_config: If False, don't save the config file (default: True)
|
|
57
59
|
"""
|
|
58
60
|
# Determine config file path
|
|
59
61
|
if config_file_path is not None:
|
|
@@ -89,6 +91,7 @@ class Bridge:
|
|
|
89
91
|
self.ip = ip
|
|
90
92
|
self._username = username
|
|
91
93
|
self.timeout = timeout
|
|
94
|
+
self.save_config = save_config
|
|
92
95
|
self.lights_by_id: dict[int, Light] = {}
|
|
93
96
|
self.lights_by_name: dict[str, Light] = {}
|
|
94
97
|
self.sensors_by_id: dict[int, Sensor] = {}
|
|
@@ -233,12 +236,13 @@ class Bridge:
|
|
|
233
236
|
for line in response:
|
|
234
237
|
for key in line:
|
|
235
238
|
if "success" in key:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
if self.save_config:
|
|
240
|
+
with open(self.config_file_path, "w") as f:
|
|
241
|
+
logger.info(
|
|
242
|
+
"Writing configuration file to " + self.config_file_path
|
|
243
|
+
)
|
|
244
|
+
f.write(json.dumps({self.ip: line["success"]}))
|
|
245
|
+
logger.info("Reconnecting to the bridge")
|
|
242
246
|
self._username = line["success"]["username"]
|
|
243
247
|
return
|
|
244
248
|
if "error" in key:
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from phue.light import Light
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger("phue_modern")
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
|
-
from
|
|
11
|
+
from phue.bridge import Bridge
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Group(Light):
|
|
@@ -4,7 +4,7 @@ import httpx
|
|
|
4
4
|
import pytest
|
|
5
5
|
import respx
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import phue
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@pytest.fixture
|
|
@@ -23,7 +23,7 @@ def test_bridge_discovery_and_registration(tmp_config_path: str) -> None:
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
# Create a bridge directly with IP and username, but use temp config
|
|
26
|
-
bridge =
|
|
26
|
+
bridge = phue.Bridge(
|
|
27
27
|
ip="192.168.1.100", username="testuser", config_file_path=tmp_config_path
|
|
28
28
|
)
|
|
29
29
|
|
|
@@ -51,7 +51,7 @@ def test_light_control(tmp_config_path: str) -> None:
|
|
|
51
51
|
)
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
bridge =
|
|
54
|
+
bridge = phue.Bridge(
|
|
55
55
|
ip="192.168.1.100", username="testuser", config_file_path=tmp_config_path
|
|
56
56
|
)
|
|
57
57
|
light = bridge.lights[0]
|
|
@@ -64,11 +64,11 @@ def test_error_handling(tmp_config_path: str) -> None:
|
|
|
64
64
|
side_effect=httpx.TimeoutException("Connection timed out")
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
-
bridge =
|
|
67
|
+
bridge = phue.Bridge(
|
|
68
68
|
ip="192.168.1.100", username="testuser", config_file_path=tmp_config_path
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
-
with pytest.raises(
|
|
71
|
+
with pytest.raises(phue.PhueRequestTimeout):
|
|
72
72
|
bridge.get_light()
|
|
73
73
|
|
|
74
74
|
|
|
@@ -83,10 +83,37 @@ def test_config_file(tmp_path: Path) -> None:
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
# Create a bridge and verify config file is created
|
|
86
|
-
bridge1 =
|
|
86
|
+
bridge1 = phue.Bridge(ip="192.168.1.100", config_file_path=str(config_file))
|
|
87
87
|
assert config_file.exists()
|
|
88
88
|
assert bridge1.username == "testuser"
|
|
89
89
|
|
|
90
90
|
# Test loading from config
|
|
91
|
-
bridge2 =
|
|
91
|
+
bridge2 = phue.Bridge(config_file_path=str(config_file))
|
|
92
92
|
assert bridge2.username == "testuser"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_no_save_config(tmp_path: Path) -> None:
|
|
96
|
+
"""Test that the config file is not saved when save_config is False."""
|
|
97
|
+
config_file = tmp_path / ".python_hue"
|
|
98
|
+
|
|
99
|
+
with respx.mock(assert_all_called=True) as mock:
|
|
100
|
+
mock.post("http://192.168.1.100/api", name="create_user").mock(
|
|
101
|
+
return_value=httpx.Response(
|
|
102
|
+
200, json=[{"success": {"username": "nosavetest"}}]
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Create a bridge with save_config=False
|
|
107
|
+
bridge = phue.Bridge(
|
|
108
|
+
ip="192.168.1.100",
|
|
109
|
+
config_file_path=str(config_file),
|
|
110
|
+
save_config=False,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Verify config file was NOT created and username is set
|
|
114
|
+
assert not config_file.exists()
|
|
115
|
+
assert bridge.username == "nosavetest"
|
|
116
|
+
|
|
117
|
+
# Verify that trying to connect again without IP fails (as config wasn't saved)
|
|
118
|
+
with pytest.raises(phue.PhueException):
|
|
119
|
+
phue.Bridge(config_file_path=str(config_file))
|
|
@@ -7,13 +7,13 @@ import httpx
|
|
|
7
7
|
import pytest
|
|
8
8
|
import respx
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from phue import Bridge
|
|
11
|
+
from phue.__main__ import main
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@pytest.fixture(autouse=True)
|
|
15
15
|
def disable_styling(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
16
|
-
monkeypatch.setattr("
|
|
16
|
+
monkeypatch.setattr("phue.__main__.DISABLE_STYLING", True)
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_cli_help() -> None:
|
|
@@ -27,8 +27,8 @@ def test_cli_help() -> None:
|
|
|
27
27
|
|
|
28
28
|
def test_cli_missing_args() -> None:
|
|
29
29
|
"""Test that the CLI errors when no host or command is provided but no config exists."""
|
|
30
|
-
with patch("
|
|
31
|
-
with patch("
|
|
30
|
+
with patch("phue.__main__.get_bridge_from_config", return_value=None):
|
|
31
|
+
with patch("phue._internal.console.console.error") as mock_error:
|
|
32
32
|
result = main([])
|
|
33
33
|
assert result == 1
|
|
34
34
|
mock_error.assert_called_once()
|
|
@@ -46,7 +46,7 @@ def test_cli_successful_connection(tmp_path: Path) -> None:
|
|
|
46
46
|
)
|
|
47
47
|
|
|
48
48
|
# Run the CLI with mocked input for the link button prompt
|
|
49
|
-
with patch("
|
|
49
|
+
with patch("phue._internal.console.console.success") as mock_success:
|
|
50
50
|
result = main(
|
|
51
51
|
["--host", "192.168.1.100", "--config-file-path", str(config_path)]
|
|
52
52
|
)
|
|
@@ -75,8 +75,8 @@ def test_cli_retry_on_registration_exception(tmp_path: Path) -> None:
|
|
|
75
75
|
# Mock the input function to simulate pressing the button
|
|
76
76
|
with (
|
|
77
77
|
patch("builtins.input", return_value=""),
|
|
78
|
-
patch("
|
|
79
|
-
patch("
|
|
78
|
+
patch("phue._internal.console.console.warning") as mock_warning,
|
|
79
|
+
patch("phue._internal.console.console.success") as mock_success,
|
|
80
80
|
):
|
|
81
81
|
result = main(
|
|
82
82
|
["--host", "192.168.1.100", "--config-file-path", str(config_path)]
|
|
@@ -105,9 +105,9 @@ def test_cli_list_command(resource: str, attribute: str) -> None:
|
|
|
105
105
|
setattr(mock_bridge, attribute, mock_items)
|
|
106
106
|
|
|
107
107
|
with (
|
|
108
|
-
patch("
|
|
109
|
-
patch("
|
|
110
|
-
patch("
|
|
108
|
+
patch("phue.__main__.Bridge", return_value=mock_bridge),
|
|
109
|
+
patch("phue.__main__.get_bridge_from_config", return_value=None),
|
|
110
|
+
patch("phue._internal.console.console.info") as mock_info,
|
|
111
111
|
):
|
|
112
112
|
result = main(["--host", "192.168.1.100", "list", resource])
|
|
113
113
|
|
|
@@ -118,51 +118,29 @@ def test_cli_list_command(resource: str, attribute: str) -> None:
|
|
|
118
118
|
|
|
119
119
|
def test_cli_get_command() -> None:
|
|
120
120
|
"""Test retrieving the details of a light."""
|
|
121
|
-
# Create mock Bridge and light
|
|
122
121
|
mock_bridge = MagicMock(spec=Bridge)
|
|
123
|
-
mock_light = MagicMock()
|
|
124
|
-
# Set properties to real values, not mocks
|
|
125
|
-
mock_light.name = "Test Light"
|
|
126
|
-
mock_light.on = True
|
|
127
|
-
mock_light.brightness = 254
|
|
128
|
-
mock_light.hue = 10000
|
|
129
|
-
mock_light.saturation = 200
|
|
130
|
-
mock_light.type = "Extended color light"
|
|
131
|
-
mock_light.reachable = True
|
|
132
|
-
mock_bridge.lights_by_name = {"Test Light": mock_light}
|
|
133
|
-
mock_bridge.lights_by_id = {}
|
|
134
122
|
|
|
135
123
|
with (
|
|
136
|
-
patch("
|
|
137
|
-
patch("
|
|
138
|
-
patch("phue2._internal.console.console.info") as mock_info,
|
|
124
|
+
patch("phue.__main__.Bridge", return_value=mock_bridge),
|
|
125
|
+
patch("phue._internal.console.console.info") as mock_info,
|
|
139
126
|
):
|
|
140
127
|
result = main(["--host", "192.168.1.100", "get", "light", "Test Light"])
|
|
141
128
|
|
|
142
129
|
assert result == 0
|
|
143
|
-
mock_info.
|
|
130
|
+
mock_info.assert_called()
|
|
144
131
|
|
|
145
132
|
|
|
146
133
|
def test_cli_set_command() -> None:
|
|
147
134
|
"""Test setting the state of a light."""
|
|
148
|
-
# Create mock Bridge and light
|
|
149
135
|
mock_bridge = MagicMock(spec=Bridge)
|
|
150
|
-
mock_light = MagicMock()
|
|
151
|
-
# Set the name property to a string, not a mock
|
|
152
|
-
mock_light.name = "Test Light"
|
|
153
|
-
mock_bridge.lights_by_id = {1: mock_light}
|
|
154
|
-
mock_bridge.lights_by_name = {}
|
|
155
136
|
|
|
156
137
|
with (
|
|
157
|
-
patch("
|
|
158
|
-
patch("
|
|
159
|
-
patch("phue2._internal.console.console.success") as mock_success,
|
|
138
|
+
patch("phue.__main__.Bridge", return_value=mock_bridge),
|
|
139
|
+
patch("phue._internal.console.console.success") as mock_success,
|
|
160
140
|
):
|
|
161
141
|
result = main(
|
|
162
142
|
["--host", "192.168.1.100", "set", "light", "1", "--on", "--bri", "200"]
|
|
163
143
|
)
|
|
164
144
|
|
|
165
145
|
assert result == 0
|
|
166
|
-
mock_success.
|
|
167
|
-
assert mock_light.on is True
|
|
168
|
-
assert mock_light.brightness == 200
|
|
146
|
+
mock_success.assert_called()
|
|
@@ -11,7 +11,7 @@ from unittest import mock
|
|
|
11
11
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import phue
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@pytest.fixture
|
|
@@ -27,9 +27,9 @@ def temp_home(tmp_path: Path) -> Generator[Path, None, None]:
|
|
|
27
27
|
def test_register(temp_home: Path):
|
|
28
28
|
"""test that registration happens automatically during setup."""
|
|
29
29
|
confname = os.path.join(temp_home, ".python_hue")
|
|
30
|
-
with mock.patch("
|
|
30
|
+
with mock.patch("phue.Bridge.request") as req:
|
|
31
31
|
req.return_value = [{"success": {"username": "fooo"}}]
|
|
32
|
-
bridge =
|
|
32
|
+
bridge = phue.Bridge(ip="10.0.0.0")
|
|
33
33
|
assert bridge.config_file_path == confname
|
|
34
34
|
|
|
35
35
|
# check contents of file
|
|
@@ -38,35 +38,35 @@ def test_register(temp_home: Path):
|
|
|
38
38
|
assert contents == '{"10.0.0.0": {"username": "fooo"}}'
|
|
39
39
|
|
|
40
40
|
# make sure we can open under a different file
|
|
41
|
-
bridge2 =
|
|
41
|
+
bridge2 = phue.Bridge(ip="10.0.0.0")
|
|
42
42
|
assert bridge2.username == "fooo"
|
|
43
43
|
|
|
44
44
|
# and that we can even open without an ip address
|
|
45
|
-
bridge3 =
|
|
45
|
+
bridge3 = phue.Bridge()
|
|
46
46
|
assert bridge3.username == "fooo"
|
|
47
47
|
assert bridge3.ip == "10.0.0.0"
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def test_register_fail():
|
|
51
51
|
"""Test that registration fails in the expected way for timeout"""
|
|
52
|
-
with mock.patch("
|
|
52
|
+
with mock.patch("phue.Bridge.request") as req:
|
|
53
53
|
req.return_value = [{"error": {"type": 101}}]
|
|
54
|
-
with pytest.raises(
|
|
55
|
-
|
|
54
|
+
with pytest.raises(phue.PhueRegistrationException):
|
|
55
|
+
phue.Bridge(ip="10.0.0.0")
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def test_register_unknown_user():
|
|
59
59
|
"""Test that registration for unknown user works."""
|
|
60
|
-
with mock.patch("
|
|
60
|
+
with mock.patch("phue.Bridge.request") as req:
|
|
61
61
|
req.return_value = [{"error": {"type": 7}}]
|
|
62
|
-
with pytest.raises(
|
|
63
|
-
|
|
62
|
+
with pytest.raises(phue.PhueException):
|
|
63
|
+
phue.Bridge(ip="10.0.0.0")
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
@pytest.fixture
|
|
67
67
|
def mock_bridge():
|
|
68
68
|
"""Fixture that provides a bridge with mocked request method."""
|
|
69
|
-
with mock.patch("
|
|
69
|
+
with mock.patch("phue.Bridge.request") as mock_request:
|
|
70
70
|
# Mock the lights collection endpoint
|
|
71
71
|
|
|
72
72
|
def mock_request_side_effect(
|
|
@@ -93,11 +93,11 @@ def mock_bridge():
|
|
|
93
93
|
|
|
94
94
|
mock_request.side_effect = mock_request_side_effect
|
|
95
95
|
|
|
96
|
-
bridge =
|
|
96
|
+
bridge = phue.Bridge(ip="10.0.0.0", username="username")
|
|
97
97
|
yield bridge
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def test_get_lights(mock_bridge:
|
|
100
|
+
def test_get_lights(mock_bridge: phue.Bridge):
|
|
101
101
|
"""Test getting light objects by ID."""
|
|
102
102
|
lights = mock_bridge.get_light_objects("id")
|
|
103
103
|
assert lights[1].name == "Living Room Bulb"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|