Skip to content

Commit ce2e7bb

Browse files
cmos486claude
andcommitted
feat: picture brightness control (number slider + select presets)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 2f1d788 commit ce2e7bb

10 files changed

Lines changed: 214 additions & 9 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ The integration creates **30+ entities** for full TV control:
150150
| `button._ircc_stop` | Stop | ⏹️ |
151151
| `button._ircc_input` | Input selector | 🔌 |
152152

153+
### 🔢 Numbers
154+
155+
| Entity | Description |
156+
|--------|-------------|
157+
| `number._brightness` | Picture brightness slider (range discovered per TV model) |
158+
159+
> Only created if the TV supports `getPictureQualitySettings` for the `brightness` target.
160+
153161
### 🎚️ Selects
154162

155163
| Entity | Options |
@@ -158,6 +166,7 @@ The integration creates **30+ entities** for full TV control:
158166
| `select._screen_rotation` | 0°, 90°, 180°, 270° |
159167
| `select._picture_mode` | Standard, Vivid, Cinema, Custom, Game, Graphics, Photo, Sports |
160168
| `select._sleep_timer` | Off, 15 min, 30 min, 45 min, 60 min, 90 min, 120 min |
169+
| `select._brightness_preset` | Min, Low, Medium, High, Max (mapped to % of brightness range) |
161170

162171
### 📊 Sensors (Diagnostic)
163172

custom_components/bravia_rest_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
Platform.MEDIA_PLAYER,
3333
Platform.REMOTE,
3434
Platform.BUTTON,
35+
Platform.NUMBER,
3536
Platform.SELECT,
3637
Platform.SENSOR,
3738
Platform.SWITCH,

custom_components/bravia_rest_api/bravia_client.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,27 @@ async def set_picture_quality_settings(
475475
[{"settings": settings}],
476476
)
477477

478+
async def get_brightness(self) -> dict[str, Any] | None:
479+
"""Get current brightness info.
480+
481+
Returns the brightness setting dict (with currentValue, min, max, etc.)
482+
or None if brightness is not supported by this TV.
483+
"""
484+
try:
485+
settings = await self.get_picture_quality_settings("brightness")
486+
except BraviaApiError:
487+
return None
488+
for item in settings:
489+
if isinstance(item, dict) and item.get("target") == "brightness":
490+
return item
491+
return None
492+
493+
async def set_brightness(self, value: int) -> None:
494+
"""Set brightness value (as integer within TV-reported range)."""
495+
await self.set_picture_quality_settings(
496+
[{"target": "brightness", "value": str(value)}]
497+
)
498+
478499
async def get_screen_rotation(self) -> int:
479500
"""Get screen rotation angle."""
480501
result = await self._request(SERVICE_VIDEO, "getScreenRotation")

custom_components/bravia_rest_api/const.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@
102102
"Sports",
103103
]
104104

105+
# Brightness presets (fraction of range 0.0-1.0)
106+
BRIGHTNESS_PRESETS: Final[dict[str, float]] = {
107+
"Min": 0.0,
108+
"Low": 0.25,
109+
"Medium": 0.5,
110+
"High": 0.75,
111+
"Max": 1.0,
112+
}
113+
105114
# WoL
106115
WOL_PORT: Final = 9
107116

custom_components/bravia_rest_api/coordinator.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class BraviaState:
4444
external_inputs: list[dict[str, Any]] = field(default_factory=list)
4545
app_list: list[dict[str, Any]] = field(default_factory=list)
4646
app_status: list[dict[str, str]] = field(default_factory=list)
47+
brightness: int | None = None
4748
is_available: bool = False
4849

4950
@property
@@ -78,6 +79,9 @@ def __init__(
7879
self.client = client
7980
self.ircc_codes = {}
8081
self.system_info = {}
82+
self.brightness_supported = False
83+
self.brightness_min = 0
84+
self.brightness_max = 100
8185
self._app_list_fetched = False
8286
self._cached_app_list = []
8387

@@ -104,6 +108,23 @@ async def async_setup(self) -> None:
104108
except BraviaError as err:
105109
_LOGGER.warning("Could not fetch IRCC codes: %s", err)
106110

111+
# Probe brightness capability (device-specific)
112+
try:
113+
info = await self.client.get_brightness()
114+
if info is not None:
115+
self.brightness_supported = True
116+
self.brightness_min = int(info.get("min", 0))
117+
self.brightness_max = int(info.get("max", 100))
118+
_LOGGER.debug(
119+
"Brightness supported: range %d-%d",
120+
self.brightness_min,
121+
self.brightness_max,
122+
)
123+
else:
124+
_LOGGER.debug("Brightness not available on this device")
125+
except BraviaError as err:
126+
_LOGGER.debug("Could not probe brightness capability: %s", err)
127+
107128
async def _async_update_data(self) -> BraviaState:
108129
"""Fetch latest state from the TV."""
109130
state = BraviaState()
@@ -167,6 +188,14 @@ async def _async_update_data(self) -> BraviaState:
167188
except BraviaError as err:
168189
_LOGGER.debug("Could not fetch app status: %s", err)
169190

191+
if self.brightness_supported:
192+
try:
193+
info = await self.client.get_brightness()
194+
if info is not None:
195+
state.brightness = int(info.get("currentValue", 0))
196+
except BraviaError as err:
197+
_LOGGER.debug("Could not fetch brightness: %s", err)
198+
170199
return state
171200

172201
def get_ircc_code(self, command: str) -> str | None:

custom_components/bravia_rest_api/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"issue_tracker": "https://github.com/cmos486/Bravia-REST-API/issues",
88
"integration_type": "device",
99
"iot_class": "local_polling",
10-
"version": "1.3.2",
10+
"version": "1.4.0",
1111
"requirements": [],
1212
"homeassistant": "2024.1.0",
1313
"ssdp": [
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Number entities for Bravia REST API."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
from homeassistant.components.number import NumberEntity, NumberMode
8+
from homeassistant.config_entries import ConfigEntry
9+
from homeassistant.core import HomeAssistant
10+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
11+
12+
from .bravia_client import BraviaError
13+
from .const import DOMAIN
14+
from .coordinator import BraviaCoordinator
15+
from .entity import BraviaEntity
16+
17+
_LOGGER = logging.getLogger(__name__)
18+
19+
20+
async def async_setup_entry(
21+
hass: HomeAssistant,
22+
entry: ConfigEntry,
23+
async_add_entities: AddEntitiesCallback,
24+
) -> None:
25+
"""Set up Bravia REST API number entities."""
26+
coordinator: BraviaCoordinator = hass.data[DOMAIN][entry.entry_id]
27+
if coordinator.brightness_supported:
28+
async_add_entities([BraviaBrightnessNumber(coordinator, entry)])
29+
30+
31+
class BraviaBrightnessNumber(BraviaEntity, NumberEntity):
32+
"""Number entity for TV picture brightness."""
33+
34+
_attr_translation_key = "brightness"
35+
_attr_icon = "mdi:brightness-6"
36+
_attr_mode = NumberMode.SLIDER
37+
_attr_native_step = 1.0
38+
39+
def __init__(
40+
self,
41+
coordinator: BraviaCoordinator,
42+
entry: ConfigEntry,
43+
) -> None:
44+
super().__init__(coordinator, entry)
45+
self._attr_unique_id = f"{entry.unique_id}_brightness"
46+
self._attr_native_min_value = float(coordinator.brightness_min)
47+
self._attr_native_max_value = float(coordinator.brightness_max)
48+
49+
@property
50+
def native_value(self) -> float | None:
51+
"""Return the current brightness value."""
52+
data = self.coordinator.data
53+
if not data or data.brightness is None:
54+
return None
55+
return float(data.brightness)
56+
57+
async def async_set_native_value(self, value: float) -> None:
58+
"""Set the brightness value."""
59+
try:
60+
await self.coordinator.client.set_brightness(int(value))
61+
except BraviaError as err:
62+
_LOGGER.error("Failed to set brightness: %s", err)
63+
await self.coordinator.async_request_refresh()

custom_components/bravia_rest_api/select.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from .bravia_client import BraviaError
1313
from .const import (
14+
BRIGHTNESS_PRESETS,
1415
DEFAULT_PICTURE_MODES,
1516
DOMAIN,
1617
SCREEN_ROTATION_OPTIONS,
@@ -30,14 +31,15 @@ async def async_setup_entry(
3031
) -> None:
3132
"""Set up Bravia REST API select entities."""
3233
coordinator: BraviaCoordinator = hass.data[DOMAIN][entry.entry_id]
33-
async_add_entities(
34-
[
35-
BraviaSoundOutputSelect(coordinator, entry),
36-
BraviaScreenRotationSelect(coordinator, entry),
37-
BraviaPictureModeSelect(coordinator, entry),
38-
BraviaSleepTimerSelect(coordinator, entry),
39-
]
40-
)
34+
entities: list[SelectEntity] = [
35+
BraviaSoundOutputSelect(coordinator, entry),
36+
BraviaScreenRotationSelect(coordinator, entry),
37+
BraviaPictureModeSelect(coordinator, entry),
38+
BraviaSleepTimerSelect(coordinator, entry),
39+
]
40+
if coordinator.brightness_supported:
41+
entities.append(BraviaBrightnessSelect(coordinator, entry))
42+
async_add_entities(entities)
4143

4244

4345
class BraviaSoundOutputSelect(BraviaEntity, SelectEntity):
@@ -247,3 +249,58 @@ async def async_added_to_hass(self) -> None:
247249
break
248250
except BraviaError:
249251
pass
252+
253+
254+
class BraviaBrightnessSelect(BraviaEntity, SelectEntity):
255+
"""Select entity for brightness presets."""
256+
257+
_attr_translation_key = "brightness_preset"
258+
_attr_icon = "mdi:brightness-6"
259+
260+
def __init__(
261+
self,
262+
coordinator: BraviaCoordinator,
263+
entry: ConfigEntry,
264+
) -> None:
265+
super().__init__(coordinator, entry)
266+
self._attr_unique_id = f"{entry.unique_id}_brightness_preset"
267+
self._attr_options = list(BRIGHTNESS_PRESETS.keys())
268+
self._bri_min = coordinator.brightness_min
269+
self._bri_max = coordinator.brightness_max
270+
271+
def _value_for_preset(self, preset: str) -> int:
272+
"""Calculate brightness value for a preset name."""
273+
fraction = BRIGHTNESS_PRESETS[preset]
274+
return round(self._bri_min + fraction * (self._bri_max - self._bri_min))
275+
276+
@property
277+
def current_option(self) -> str | None:
278+
"""Return the closest brightness preset to the current value."""
279+
data = self.coordinator.data
280+
if not data or data.brightness is None:
281+
return None
282+
current = data.brightness
283+
bri_range = self._bri_max - self._bri_min
284+
if bri_range == 0:
285+
return "Min"
286+
fraction = (current - self._bri_min) / bri_range
287+
closest = "Medium"
288+
closest_dist = float("inf")
289+
for name, pct in BRIGHTNESS_PRESETS.items():
290+
dist = abs(fraction - pct)
291+
if dist < closest_dist:
292+
closest_dist = dist
293+
closest = name
294+
return closest
295+
296+
async def async_select_option(self, option: str) -> None:
297+
"""Set brightness to a preset value."""
298+
if option not in BRIGHTNESS_PRESETS:
299+
_LOGGER.error("Unknown brightness preset: %s", option)
300+
return
301+
value = self._value_for_preset(option)
302+
try:
303+
await self.coordinator.client.set_brightness(value)
304+
except BraviaError as err:
305+
_LOGGER.error("Failed to set brightness preset: %s", err)
306+
await self.coordinator.async_request_refresh()

custom_components/bravia_rest_api/strings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
"name": "Picture On"
5353
}
5454
},
55+
"number": {
56+
"brightness": {
57+
"name": "Brightness"
58+
}
59+
},
5560
"select": {
5661
"sound_output": {
5762
"name": "Sound Output"
@@ -64,6 +69,9 @@
6469
},
6570
"sleep_timer": {
6671
"name": "Sleep Timer"
72+
},
73+
"brightness_preset": {
74+
"name": "Brightness Preset"
6775
}
6876
},
6977
"sensor": {

custom_components/bravia_rest_api/translations/es.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
"name": "Encender Pantalla"
5353
}
5454
},
55+
"number": {
56+
"brightness": {
57+
"name": "Brillo"
58+
}
59+
},
5560
"select": {
5661
"sound_output": {
5762
"name": "Salida de Audio"
@@ -64,6 +69,9 @@
6469
},
6570
"sleep_timer": {
6671
"name": "Temporizador de Sue\u00f1o"
72+
},
73+
"brightness_preset": {
74+
"name": "Preajuste de Brillo"
6775
}
6876
},
6977
"sensor": {

0 commit comments

Comments
 (0)