3
3
from __future__ import annotations
4
4
5
5
import logging
6
- import math
7
- from collections import OrderedDict
8
6
from types import MappingProxyType
9
7
from typing import Any
10
8
62
60
)
63
61
64
62
65
- def _convert_percent_to_brightness (percent : int ) -> int :
66
- return 0 if percent == 0 else value_to_brightness ((1 , 100 ), percent )
67
-
68
-
69
63
async def async_setup_entry (
70
64
hass : HomeAssistant ,
71
65
config_entry : ConfigEntry ,
@@ -282,8 +276,6 @@ def async_update_group_state(self) -> None:
282
276
# Flag is this update is caused by this Lightener when calling turn_on.
283
277
is_lightener_change = False
284
278
285
- current_state = self .hass .states .get (self .entity_id )
286
-
287
279
# Let the Group integration make its magic, which includes recalculating the brightness.
288
280
super ().async_update_group_state ()
289
281
@@ -396,88 +388,18 @@ def __init__(
396
388
397
389
self .entity_id = entity_id
398
390
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
- )
465
391
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" , {}))
473
394
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
+ )
481
403
482
404
@property
483
405
def type (self ) -> str | None :
@@ -491,18 +413,137 @@ def type(self) -> str | None:
491
413
def translate_brightness (self , brightness : int ) -> int :
492
414
"""Calculate the entitiy brightness for the give Lightener brightness level."""
493
415
416
+ level = self .levels .get (int (brightness ))
417
+
494
418
if self .type == TYPE_ONOFF :
495
- return self . levels_on_off [ int ( brightness )]
419
+ return 0 if level == 0 else 255
496
420
497
- return self . levels [ int ( brightness )]
421
+ return level
498
422
499
423
def translate_brightness_back (self , brightness : int ) -> list [int ]:
500
424
"""Calculate all possible Lightener brightness levels for a give entity brightness."""
501
425
502
426
if brightness is None :
503
427
return []
504
428
429
+ levels = self .to_lightener_levels .get (int (brightness ))
430
+
505
431
if self .type == TYPE_ONOFF :
506
432
return self .to_lightener_levels_on_off [int (brightness )]
507
433
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