Skip to content

Commit 4da6da1

Browse files
committed
Revamped the levels mapping logic making it simpler and more precise
1 parent 8849896 commit 4da6da1

File tree

2 files changed

+360
-117
lines changed

2 files changed

+360
-117
lines changed

custom_components/lightener/light.py

Lines changed: 132 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
from __future__ import annotations
44

55
import logging
6-
import math
7-
from collections import OrderedDict
86
from types import MappingProxyType
97
from typing import Any
108

@@ -62,10 +60,6 @@
6260
)
6361

6462

65-
def _convert_percent_to_brightness(percent: int) -> int:
66-
return 0 if percent == 0 else value_to_brightness((1, 100), percent)
67-
68-
6963
async def async_setup_entry(
7064
hass: HomeAssistant,
7165
config_entry: ConfigEntry,
@@ -282,8 +276,6 @@ def async_update_group_state(self) -> None:
282276
# Flag is this update is caused by this Lightener when calling turn_on.
283277
is_lightener_change = False
284278

285-
current_state = self.hass.states.get(self.entity_id)
286-
287279
# Let the Group integration make its magic, which includes recalculating the brightness.
288280
super().async_update_group_state()
289281

@@ -396,88 +388,18 @@ def __init__(
396388

397389
self.entity_id = entity_id
398390
self.hass = hass
399-
self._type = None
400-
401-
config_levels = {}
402-
403-
for lightener_level, entity_value in config.get("brightness", {}).items():
404-
config_levels[
405-
_convert_percent_to_brightness(int(lightener_level))
406-
] = _convert_percent_to_brightness(int(entity_value))
407-
408-
config_levels.setdefault(255, 255)
409-
410-
config_levels = OrderedDict(sorted(config_levels.items()))
411-
412-
# Start the level list with value 0 for level 0.
413-
levels = [0]
414-
levels_on_off = [0]
415-
416-
# List with all possible Lightener levels for a given entity level.
417-
# Initializa it with a list from 0 to 255 having each entry an empty array.
418-
to_lightener_levels = [[] for i in range(0, 256)]
419-
to_lightener_levels_on_off = [[] for i in range(0, 256)]
420-
421-
previous_lightener_level = 0
422-
previous_light_level = 0
423-
424-
# Fill all levels with the calculated values between the ranges.
425-
for lightener_level, light_level in config_levels.items():
426-
# Calculate all possible levels between the configured ranges
427-
# to be used during translation (lightener -> entity)
428-
for i in range(previous_lightener_level + 1, lightener_level):
429-
value_at_current_level = math.ceil(
430-
previous_light_level
431-
+ (light_level - previous_light_level)
432-
* (i - previous_lightener_level)
433-
/ (lightener_level - previous_lightener_level)
434-
)
435-
levels.append(value_at_current_level)
436-
to_lightener_levels[value_at_current_level].append(i)
437-
438-
# On/Off entities have only two possible levels: 0 (off) and 255 (on).
439-
levels_on_off.append(255 if value_at_current_level > 0 else 0)
440-
to_lightener_levels_on_off[
441-
255 if value_at_current_level > 0 else 0
442-
].append(i)
443-
444-
# To account for rounding, we use the configured values directly.
445-
levels.append(light_level)
446-
to_lightener_levels[light_level].append(lightener_level)
447-
448-
levels_on_off.append(255 if light_level > 0 else 0)
449-
to_lightener_levels_on_off[255 if light_level > 0 else 0].append(
450-
lightener_level
451-
)
452-
453-
# Do the reverse calculation for the oposite translation direction (entity -> lightener)
454-
for i in range(
455-
previous_light_level,
456-
light_level,
457-
1 if previous_light_level < light_level else -1,
458-
):
459-
value_at_current_level = math.ceil(
460-
previous_lightener_level
461-
+ (lightener_level - previous_lightener_level)
462-
* (i - previous_light_level)
463-
/ (light_level - previous_light_level)
464-
)
465391

466-
# Since the same entity level can happen more than once (e.g. "50:100, 100:0") we
467-
# create a list with all possible lightener levels at this (i) entity brightness.
468-
if value_at_current_level not in to_lightener_levels[i]:
469-
to_lightener_levels[i].append(value_at_current_level)
470-
to_lightener_levels_on_off[
471-
255 if value_at_current_level > 0 else 0
472-
].append(value_at_current_level)
392+
# Get the brightness configuration and prepare it for processing,
393+
brightness_config = prepare_brightness_config(config.get("brightness", {}))
473394

474-
previous_lightener_level = lightener_level
475-
previous_light_level = light_level
476-
477-
self.levels = levels
478-
self.to_lightener_levels = to_lightener_levels
479-
self.levels_on_off = levels_on_off
480-
self.to_lightener_levels_on_off = to_lightener_levels_on_off
395+
# Create the brightness conversion maps (from lightener to entity and from entity to lightener).
396+
self.levels = create_brightness_map(brightness_config)
397+
self.to_lightener_levels = create_reverse_brightness_map(
398+
brightness_config, self.levels
399+
)
400+
self.to_lightener_levels_on_off = create_reverse_brightness_map_on_off(
401+
self.to_lightener_levels
402+
)
481403

482404
@property
483405
def type(self) -> str | None:
@@ -491,18 +413,137 @@ def type(self) -> str | None:
491413
def translate_brightness(self, brightness: int) -> int:
492414
"""Calculate the entitiy brightness for the give Lightener brightness level."""
493415

416+
level = self.levels.get(int(brightness))
417+
494418
if self.type == TYPE_ONOFF:
495-
return self.levels_on_off[int(brightness)]
419+
return 0 if level == 0 else 255
496420

497-
return self.levels[int(brightness)]
421+
return level
498422

499423
def translate_brightness_back(self, brightness: int) -> list[int]:
500424
"""Calculate all possible Lightener brightness levels for a give entity brightness."""
501425

502426
if brightness is None:
503427
return []
504428

429+
levels = self.to_lightener_levels.get(int(brightness))
430+
505431
if self.type == TYPE_ONOFF:
506432
return self.to_lightener_levels_on_off[int(brightness)]
507433

508-
return self.to_lightener_levels[int(brightness)]
434+
return levels
435+
436+
437+
def translate_config_to_brightness(config: dict) -> dict:
438+
"""Create a copy of config converting the 0-100 range to 1-255.
439+
440+
Convert the values to integers since the original values are strings.
441+
"""
442+
443+
return {
444+
value_to_brightness((1, 100), int(k)): 0
445+
if int(v) == 0
446+
else value_to_brightness((1, 100), int(v))
447+
for k, v in config.items()
448+
}
449+
450+
451+
def prepare_brightness_config(config: dict) -> dict:
452+
"""Convert the brightness configuration to a list of tuples and sorts it by the lightener level.
453+
454+
Also add the default 0 and 255 levels if they are not present.
455+
"""
456+
457+
config = translate_config_to_brightness(config)
458+
459+
# Zero must always be zero.
460+
config[0] = 0
461+
462+
# If the maximum level is not present, add it.
463+
config.setdefault(255, 255)
464+
465+
# Transform the dictionary into a list of tuples and sort it by the lightener level.
466+
config = sorted(config.items())
467+
468+
return config
469+
470+
471+
def create_brightness_map(config: list) -> dict:
472+
"""Create a mapping of lightener levels to entity levels."""
473+
474+
brightness_map = {0: 0}
475+
476+
for i in range(1, len(config)):
477+
start, end = config[i - 1][0], config[i][0]
478+
start_value, end_value = config[i - 1][1], config[i][1]
479+
for j in range(start + 1, end + 1):
480+
brightness_map[j] = scale_ranged_value_to_int_range(
481+
(start, end), (start_value, end_value), j
482+
)
483+
484+
return brightness_map
485+
486+
487+
def create_reverse_brightness_map(config: list, lightener_levels: dict) -> dict:
488+
"""Create a map with all entity level (from 0 to 255) to all possible lightener levels at each entity level.
489+
490+
There can be multiple lightener levels for a single entity level.
491+
"""
492+
493+
# Initialize with all levels from 0 to 255.
494+
reverse_brightness_map = {i: [] for i in range(256)}
495+
496+
# Initialize entries with all lightener levels (it goes from 0 to 255)
497+
for k, v in lightener_levels.items():
498+
reverse_brightness_map[v].append(k)
499+
500+
# Now fill the gaps in the map by looping though the configured entity ranges
501+
for i in range(1, len(config)):
502+
start, end = config[i - 1][0], config[i][0]
503+
start_value, end_value = config[i - 1][1], config[i][1]
504+
505+
# If there is an entity range to be covered
506+
if start_value != end_value:
507+
order = 1 if start_value < end_value else -1
508+
509+
# Loop through the entity range
510+
for j in range(start_value, end_value + order, order):
511+
entity_level = scale_ranged_value_to_int_range(
512+
(start_value, end_value), (start, end), j
513+
)
514+
# If the entry is not yet present for into that level, add it.
515+
if entity_level not in reverse_brightness_map[j]:
516+
reverse_brightness_map[j].append(entity_level)
517+
518+
return reverse_brightness_map
519+
520+
521+
def create_reverse_brightness_map_on_off(reverse_map: dict) -> dict:
522+
"""Create a reversed map dedicated to on/off lights."""
523+
524+
# Build the "on" state out of all levels which are not in the "off" state.
525+
on_levels = [i for i in range(1, 256) if i not in reverse_map[0]]
526+
527+
# The "on" levels are possible for all non-zero levels.
528+
reverse_map_on_off = {i: on_levels for i in range(1, 256)}
529+
530+
# The "off" matches the normal reverse map.
531+
reverse_map_on_off[0] = reverse_map[0]
532+
533+
return reverse_map_on_off
534+
535+
536+
def scale_ranged_value_to_int_range(
537+
source_range: tuple[float, float],
538+
target_range: tuple[float, float],
539+
value: float,
540+
) -> int:
541+
"""Scale a value from one range to another and return an integer."""
542+
543+
# Unpack the original and target ranges
544+
(a, b) = source_range
545+
(c, d) = target_range
546+
547+
# Calculate the conversion
548+
y = c + ((value - a) * (d - c)) / (b - a)
549+
return round(y)

0 commit comments

Comments
 (0)