Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ <h2 class="section-title" id="header-classes">Classes</h2>
}
if self.session is None:
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
AUTHENTICATION_ENDPOINT, json=dataDictionary
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
AUTHENTICATION_ENDPOINT, json=dataDictionary
) as resp:
Expand All @@ -132,12 +132,12 @@ <h2 class="section-title" id="header-classes">Classes</h2>
raise
if self.session is None:
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(uri, headers=headers) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.get(uri, headers=headers) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
Expand All @@ -163,7 +163,7 @@ <h2 class="section-title" id="header-classes">Classes</h2>
}
if self.session is None:
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
CONTROLLER_AUTHORIZATION_ENDPOINT,
headers=headers,
Expand All @@ -172,7 +172,7 @@ <h2 class="section-title" id="header-classes">Classes</h2>
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
CONTROLLER_AUTHORIZATION_ENDPOINT,
headers=headers,
Expand Down
12 changes: 6 additions & 6 deletions docs/auth.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ <h1 class="title">Module <code>pyControl4.auth</code></h1>
}
}
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
AUTHENTICATION_ENDPOINT, json=dataDictionary
) as resp:
Expand All @@ -96,7 +96,7 @@ <h1 class="title">Module <code>pyControl4.auth</code></h1>
_LOGGER.error(msg)
raise
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(uri, headers=headers) as resp:
return await resp.text()

Expand All @@ -121,7 +121,7 @@ <h1 class="title">Module <code>pyControl4.auth</code></h1>
}
}
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
CONTROLLER_AUTHORIZATION_ENDPOINT,
headers=headers,
Expand Down Expand Up @@ -276,7 +276,7 @@ <h2 class="section-title" id="header-classes">Classes</h2>
}
}
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
AUTHENTICATION_ENDPOINT, json=dataDictionary
) as resp:
Expand All @@ -297,7 +297,7 @@ <h2 class="section-title" id="header-classes">Classes</h2>
_LOGGER.error(msg)
raise
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(uri, headers=headers) as resp:
return await resp.text()

Expand All @@ -322,7 +322,7 @@ <h2 class="section-title" id="header-classes">Classes</h2>
}
}
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
CONTROLLER_AUTHORIZATION_ENDPOINT,
headers=headers,
Expand Down
16 changes: 8 additions & 8 deletions docs/director.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ <h2 class="section-title" id="header-classes">Classes</h2>
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False)
) as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(
self.base_url + uri, headers=self.headers
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.get(
self.base_url + uri, headers=self.headers
) as resp:
Expand Down Expand Up @@ -135,14 +135,14 @@ <h2 class="section-title" id="header-classes">Classes</h2>
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False)
) as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
self.base_url + uri, headers=self.headers, json=dataDictionary
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
self.base_url + uri, headers=self.headers, json=dataDictionary
) as resp:
Expand Down Expand Up @@ -799,14 +799,14 @@ <h2 id="returns">Returns</h2>
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False)
) as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(
self.base_url + uri, headers=self.headers
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.get(
self.base_url + uri, headers=self.headers
) as resp:
Expand Down Expand Up @@ -849,14 +849,14 @@ <h2 id="parameters">Parameters</h2>
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False)
) as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
self.base_url + uri, headers=self.headers, json=dataDictionary
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
self.base_url + uri, headers=self.headers, json=dataDictionary
) as resp:
Expand Down
12 changes: 6 additions & 6 deletions pyControl4/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import async_timeout
import json
import logging
import datetime

Check warning on line 9 in pyControl4/account.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

F401 'datetime' imported but unused

from .error_handling import checkResponseForError

Expand Down Expand Up @@ -64,14 +64,14 @@
}
if self.session is None:
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
AUTHENTICATION_ENDPOINT, json=dataDictionary
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
AUTHENTICATION_ENDPOINT, json=dataDictionary
) as resp:
Expand All @@ -94,12 +94,12 @@
raise
if self.session is None:
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(uri, headers=headers) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.get(uri, headers=headers) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
Expand All @@ -125,7 +125,7 @@
}
if self.session is None:
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
CONTROLLER_AUTHORIZATION_ENDPOINT,
headers=headers,
Expand All @@ -134,7 +134,7 @@
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
CONTROLLER_AUTHORIZATION_ENDPOINT,
headers=headers,
Expand Down
8 changes: 4 additions & 4 deletions pyControl4/director.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ async def sendGetRequest(self, uri):
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False)
) as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.get(
self.base_url + uri, headers=self.headers
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.get(
self.base_url + uri, headers=self.headers
) as resp:
Expand Down Expand Up @@ -86,14 +86,14 @@ async def sendPostRequest(self, uri, command, params, async_variable=True):
async with aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False)
) as session:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with session.post(
self.base_url + uri, headers=self.headers, json=dataDictionary
) as resp:
await checkResponseForError(await resp.text())
return await resp.text()
else:
with async_timeout.timeout(10):
async with async_timeout.timeout(10):
async with self.session.post(
self.base_url + uri, headers=self.headers, json=dataDictionary
) as resp:
Expand Down
115 changes: 115 additions & 0 deletions pyControl4/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,118 @@
"RAMP_TO_LEVEL",
{"LEVEL": level, "TIME": time},
)

async def setColorXY(
self, x: float, y: float, *, rate: int | None = None, mode: int = 0
):
"""
Sends SET_COLOR_TARGET with xy and mode.
- x, y: CIE 1931 coordinates (0..1 ~ typically)
- rate: ramp duration in milliseconds (Optional)
- mode: 0 = full color, 1 = CCT
"""
params = {
"LIGHT_COLOR_TARGET_X": float(x),
"LIGHT_COLOR_TARGET_Y": float(y),
"LIGHT_COLOR_TARGET_MODE": int(mode),
}
if rate is not None:
params["RATE"] = int(rate)

await self.director.sendPostRequest(
f"/api/v1/items/{self.item_id}/commands",
"SET_COLOR_TARGET",
params,
)

async def setColorRGB(self, r: int, g: int, b: int, *, rate: int | None = None):
"""RGB 0..255 -> xy, mode=0 (full color)."""
x, y = self._rgb_to_xy(r, g, b)
await self.setColorXY(x, y, rate=rate, mode=0)

async def setColorHex(self, hex_color: str, *, rate: int | None = None):
"""HEX (#RRGGBB/#RGB/RRGGBB/RGB) -> xy, mode=0 (full color)."""
r, g, b = self._hex_to_rgb(hex_color)
await self.setColorRGB(r, g, b, rate=rate)

async def setColorTemperature(self, kelvin: int, *, rate: int | None = None):
"""Kelvin -> xy, mode=1 (CCT)."""
x, y = self._cct_to_xy(kelvin)
await self.setColorXY(x, y, rate=rate, mode=1)

# ---------- Color Utilities ----------
@staticmethod
def _hex_to_rgb(color: str) -> tuple[int, int, int]:
s = color.strip()
if s.startswith("#"):
s = s[1:]
if len(s) == 3:
s = "".join(c * 2 for c in s)
if len(s) != 6:
raise ValueError("HEX color must be RRGGBB, #RRGGBB, #RGB or RGB")
r = int(s[0:2], 16)
g = int(s[2:4], 16)
b = int(s[4:6], 16)
return r, g, b

@staticmethod
def _srgb_to_linear(c: float) -> float:
# c in [0..1]
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4

@classmethod
def _rgb_to_xy(cls, r: int, g: int, b: int) -> tuple[float, float]:
# Normalize 0..255 -> 0..1 sRGB
rs = r / 255.0
gs = g / 255.0
bs = b / 255.0
# Correction gamma sRGB -> lin
rlin = cls._srgb_to_linear(rs)
glin = cls._srgb_to_linear(gs)
blin = cls._srgb_to_linear(bs)
# lin RGB -> XYZ (D65)
X = rlin * 0.4124 + glin * 0.3576 + blin * 0.1805
Y = rlin * 0.2126 + glin * 0.7152 + blin * 0.0722
Z = rlin * 0.0193 + glin * 0.1192 + blin * 0.9505
denom = X + Y + Z
if denom <= 1e-9:
return 0.3127, 0.3290 # fallback D65 if complete black
x = X / denom
y = Y / denom
# Optionally round to 4 decimal places (many drivers prefer this)
return round(x, 4), round(y, 4)

@staticmethod
def _cct_to_xy(kelvin: int) -> tuple[float, float]:
"""Approximation CIE 1931 xy for 1667K..25000K (classic formulas)."""
K = float(kelvin)
if K < 1667:
K = 1667.0
if K > 25000:
K = 25000.0

# x as a function of K
if 1667 <= K <= 4000:
x = (
(-0.2661239 * 1e9) / (K**3)
- (0.2343580 * 1e6) / (K**2)

Check warning on line 140 in pyControl4/light.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

W503 line break before binary operator
+ (0.8776956 * 1e3) / K

Check warning on line 141 in pyControl4/light.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

W503 line break before binary operator
+ 0.179910

Check warning on line 142 in pyControl4/light.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

W503 line break before binary operator
)
else: # 4000..25000
x = (
(-3.0258469 * 1e9) / (K**3)
+ (2.1070379 * 1e6) / (K**2)

Check warning on line 147 in pyControl4/light.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

W503 line break before binary operator
+ (0.2226347 * 1e3) / K

Check warning on line 148 in pyControl4/light.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

W503 line break before binary operator
+ 0.240390

Check warning on line 149 in pyControl4/light.py

View workflow job for this annotation

GitHub Actions / Lint code with flake8

W503 line break before binary operator
)

# y as a function of x and K
if 1667 <= K <= 2222:
y = -1.1063814 * x**3 - 1.34811020 * x**2 + 2.18555832 * x - 0.20219683
elif 2222 < K <= 4000:
y = -0.9549476 * x**3 - 1.37418593 * x**2 + 2.09137015 * x - 0.16748867
else: # 4000..25000
y = 3.0817580 * x**3 - 5.87338670 * x**2 + 3.75112997 * x - 0.37001483

return round(x, 4), round(y, 4)
Loading
Loading