2525from flux_led .utils import rgbw_brightness
2626
2727from led_ble .model_db import LEDBLEModel
28+ from led_ble .protocol import ProtocolFairy
2829
2930from .const import (
31+ HELLO_FAIRY_READ_CHARACTERISTIC ,
3032 POSSIBLE_READ_CHARACTERISTIC_UUIDS ,
3133 POSSIBLE_WRITE_CHARACTERISTIC_UUIDS ,
3234 STATE_COMMAND ,
@@ -279,10 +281,14 @@ def _generate_preset_pattern(
279281 brightness = int (brightness * 255 / 100 )
280282 speed = int (speed * 255 / 100 )
281283 return bytearray ([0x9E , 0x00 , pattern , speed , brightness , 0x00 , 0xE9 ])
282- PresetPattern .valid_or_raise (pattern )
284+ if not self ._is_hello_fairy ():
285+ PresetPattern .valid_or_raise (pattern )
283286 if not (1 <= brightness <= 100 ):
284287 raise ValueError ("Brightness must be between 1 and 100" )
285288 assert self ._protocol is not None # nosec
289+ if self ._is_hello_fairy () and pattern > 58 :
290+ rgb = [[255 , 0 , 0 ], [0 , 255 , 0 ], [0 , 0 , 255 ]] * 8 + [[255 , 0 , 0 ]]
291+ return self ._protocol .construct_custom_effect (rgb , speed , "" )
286292 return self ._protocol .construct_preset_pattern (pattern , speed , brightness )
287293
288294 async def async_set_preset_pattern (
@@ -431,29 +437,64 @@ def _named_effect(self) -> str | None:
431437 """Returns the named effect."""
432438 return EFFECT_ID_NAME .get (self .preset_pattern_num )
433439
440+ # ideally replace with classes to encapsulate the differences between device makes
441+ def _is_hello_fairy (self ) -> bool :
442+ if self ._read_char is None :
443+ return False
444+ d = self ._read_char .descriptors
445+ c = d [0 ].characteristic_uuid if (len (d ) > 0 ) else None
446+ return c == HELLO_FAIRY_READ_CHARACTERISTIC
447+
434448 def _notification_handler (self , _sender : int , data : bytearray ) -> None :
435449 """Handle notification responses."""
436450 _LOGGER .debug ("%s: Notification received: %s" , self .name , data .hex ())
437451
438- if len (data ) == 4 and data [0 ] == 0xCC :
439- on = data [1 ] == 0x23
440- self ._state = replace (self ._state , power = on )
441- return
442- if len (data ) < 11 :
443- return
444- model_num = data [1 ]
445- on = data [2 ] == 0x23
446- preset_pattern = data [3 ]
447- mode = data [4 ]
448- speed = data [5 ]
449- r = data [6 ]
450- g = data [7 ]
451- b = data [8 ]
452- w = data [9 ]
453- version = data [10 ]
454- self ._state = LEDBLEState (
455- on , (r , g , b ), w , model_num , preset_pattern , mode , speed , version
456- )
452+ model_num = 0
453+ if self ._is_hello_fairy ():
454+ if data [0 ] == 0xAA :
455+ if data [1 ] == 0x00 : # hw info
456+ if len (data ) > 7 :
457+ version_string = data [3 :8 ].decode ("ascii" )
458+ _LOGGER .debug ("version %s" , version_string )
459+ self ._state = replace (
460+ self ._state ,
461+ version_num = (data [3 ] - 48 ) * 100
462+ + (data [5 ] - 48 ) * 10
463+ + (data [7 ] - 48 ),
464+ )
465+ if len (data ) > 12 :
466+ model = data [8 :13 ].decode ("ascii" )
467+ _LOGGER .debug ("model %s" , model )
468+ if len (data ) > 24 :
469+ lights = data [24 ] # guessing
470+ _LOGGER .debug ("lights %d" , lights )
471+ if len (data ) > 33 :
472+ effects = data [33 ] # guessing
473+ _LOGGER .debug ("effects %d" , effects )
474+
475+ if data [1 ] == 0x01 : # state info
476+ if len (data ) > 6 :
477+ self ._state = replace (self ._state , power = data [6 ] > 0 )
478+ else :
479+ if len (data ) == 4 and data [0 ] == 0xCC :
480+ on = data [1 ] == 0x23
481+ self ._state = replace (self ._state , power = on )
482+ return
483+ if len (data ) < 11 :
484+ return
485+ model_num = data [1 ]
486+ on = data [2 ] == 0x23
487+ preset_pattern = data [3 ]
488+ mode = data [4 ]
489+ speed = data [5 ]
490+ r = data [6 ]
491+ g = data [7 ]
492+ b = data [8 ]
493+ w = data [9 ]
494+ version = data [10 ]
495+ self ._state = LEDBLEState (
496+ on , (r , g , b ), w , model_num , preset_pattern , mode , speed , version
497+ )
457498
458499 _LOGGER .debug (
459500 "%s: Notification received; RSSI: %s: %s %s" ,
@@ -466,8 +507,10 @@ def _notification_handler(self, _sender: int, data: bytearray) -> None:
466507 if not self ._resolve_protocol_event .is_set ():
467508 self ._resolve_protocol_event .set ()
468509 self ._model_data = get_model (model_num )
469- self ._set_protocol (self ._model_data .protocol_for_version_num (version ))
470-
510+ if self ._is_hello_fairy ():
511+ self ._protocol = ProtocolFairy ()
512+ else :
513+ self ._set_protocol (self ._model_data .protocol_for_version_num (version ))
471514 self ._fire_callbacks ()
472515
473516 def _reset_disconnect_timer (self ) -> None :
@@ -622,13 +665,23 @@ def _resolve_characteristics(self, services: BleakGATTServiceCollection) -> bool
622665 if char := services .get_characteristic (characteristic ):
623666 self ._write_char = char
624667 break
668+ _LOGGER .debug (
669+ "using characteristic %s for read, characteristic %s for write" ,
670+ self ._read_char ,
671+ self ._write_char ,
672+ )
625673 return bool (self ._read_char and self ._write_char )
626674
627675 async def _resolve_protocol (self ) -> None :
628676 """Resolve protocol."""
629677 if self ._resolve_protocol_event .is_set ():
630678 return
631- await self ._send_command_while_connected ([STATE_COMMAND ])
679+ if self ._is_hello_fairy ():
680+ await self ._send_command_while_connected (
681+ [b"\xaa \x00 \x00 \xaa " ]
682+ ) # get version and capabilities
683+ else :
684+ await self ._send_command_while_connected ([STATE_COMMAND ])
632685 async with asyncio_timeout (10 ):
633686 await self ._resolve_protocol_event .wait ()
634687
0 commit comments