44import asyncio
55import binascii
66import logging
7+ import time
8+ from dataclasses import replace
79from enum import Enum
8- from typing import Any , Callable
10+ from typing import Any , Callable , TypeVar , cast
911from uuid import UUID
1012
1113import async_timeout
@@ -53,6 +55,13 @@ class ColorMode(Enum):
5355 EFFECT = 3
5456
5557
58+ # If the scanner is in passive mode, we
59+ # need to poll the device to get the
60+ # battery and a few rarely updating
61+ # values.
62+ PASSIVE_POLL_INTERVAL = 60 * 60 * 24
63+
64+
5665class CharacteristicMissingError (Exception ):
5766 """Raised when a characteristic is missing."""
5867
@@ -76,6 +85,32 @@ def _sb_uuid(comms_type: str = "service") -> UUID | str:
7685WRITE_CHAR_UUID = _sb_uuid (comms_type = "tx" )
7786
7887
88+ WrapFuncType = TypeVar ("WrapFuncType" , bound = Callable [..., Any ])
89+
90+
91+ def update_after_operation (func : WrapFuncType ) -> WrapFuncType :
92+ """Define a wrapper to update after an operation."""
93+
94+ async def _async_update_after_operation_wrap (
95+ self : SwitchbotBaseDevice , * args : Any , ** kwargs : Any
96+ ) -> None :
97+ ret = await func (self , * args , ** kwargs )
98+ await self .update ()
99+ self ._fire_callbacks ()
100+ return ret
101+
102+ return cast (WrapFuncType , _async_update_after_operation_wrap )
103+
104+
105+ def _merge_data (old_data : dict [str , Any ], new_data : dict [str , Any ]) -> dict [str , Any ]:
106+ """Merge data but only add None keys if they are missing."""
107+ merged = old_data .copy ()
108+ for key , value in new_data .items ():
109+ if value is not None or key not in old_data :
110+ merged [key ] = value
111+ return merged
112+
113+
79114class SwitchbotBaseDevice :
80115 """Base Representation of a Switchbot Device."""
81116
@@ -109,6 +144,7 @@ def __init__(
109144 self .loop = asyncio .get_event_loop ()
110145 self ._callbacks : list [Callable [[], None ]] = []
111146 self ._notify_future : asyncio .Future [bytearray ] | None = None
147+ self ._last_full_update : float = - PASSIVE_POLL_INTERVAL
112148
113149 def advertisement_changed (self , advertisement : SwitchBotAdvertisement ) -> bool :
114150 """Check if the advertisement has changed."""
@@ -190,6 +226,18 @@ def name(self) -> str:
190226 """Return device name."""
191227 return f"{ self ._device .name } ({ self ._device .address } )"
192228
229+ @property
230+ def data (self ) -> dict [str , Any ]:
231+ """Return device data."""
232+ if self ._sb_adv_data :
233+ return self ._sb_adv_data .data
234+ return {}
235+
236+ @property
237+ def parsed_data (self ) -> dict [str , Any ]:
238+ """Return parsed device data."""
239+ return self .data .get ("data" ) or {}
240+
193241 @property
194242 def rssi (self ) -> int :
195243 """Return RSSI of device."""
@@ -392,6 +440,7 @@ def _override_state(self, state: dict[str, Any]) -> None:
392440 if self ._override_adv_data is None :
393441 self ._override_adv_data = {}
394442 self ._override_adv_data .update (state )
443+ self ._update_parsed_data (state )
395444
396445 def _get_adv_value (self , key : str ) -> Any :
397446 """Return value from advertisement data."""
@@ -466,8 +515,20 @@ def _unsub() -> None:
466515
467516 return _unsub
468517
469- async def update (self ) -> None :
470- """Update state of device."""
518+ async def update (self , interface : int | None = None ) -> None :
519+ """Update position, battery percent and light level of device."""
520+ if info := await self .get_basic_info ():
521+ self ._last_full_update = time .monotonic ()
522+ self ._update_parsed_data (info )
523+
524+ async def get_basic_info (self ) -> dict [str , Any ] | None :
525+ """Get device basic settings."""
526+ if not (_data := await self ._get_basic_info ()):
527+ return None
528+ return {
529+ "battery" : _data [1 ],
530+ "firmware" : _data [2 ] / 10.0 ,
531+ }
471532
472533 def _check_command_result (
473534 self , result : bytes | None , index : int , values : set [int ]
@@ -480,21 +541,54 @@ def _check_command_result(
480541 )
481542 return result [index ] in values
482543
544+ def _update_parsed_data (self , new_data : dict [str , Any ]) -> None :
545+ """Update data."""
546+ if not self ._sb_adv_data :
547+ _LOGGER .exception ("No advertisement data to update" )
548+ return
549+ self ._set_parsed_data (
550+ self ._sb_adv_data ,
551+ _merge_data (self ._sb_adv_data .data .get ("data" ) or {}, new_data ),
552+ )
553+
554+ def _set_parsed_data (
555+ self , advertisement : SwitchBotAdvertisement , data : dict [str , Any ]
556+ ) -> None :
557+ """Set data."""
558+ self ._sb_adv_data = replace (
559+ advertisement , data = self ._sb_adv_data .data | {"data" : data }
560+ )
561+
483562 def _set_advertisement_data (self , advertisement : SwitchBotAdvertisement ) -> None :
484563 """Set advertisement data."""
485- if (
486- advertisement .data .get ("data" )
487- or not self ._sb_adv_data
488- or not self ._sb_adv_data .data .get ("data" )
489- ):
564+ new_data = advertisement .data .get ("data" ) or {}
565+ if advertisement .active :
566+ # If we are getting active data, we can assume we are
567+ # getting active scans and we do not need to poll
568+ self ._last_full_update = time .monotonic ()
569+ if not self ._sb_adv_data :
490570 self ._sb_adv_data = advertisement
571+ elif new_data :
572+ self ._update_parsed_data (new_data )
491573 self ._override_adv_data = None
492574
493575 def switch_mode (self ) -> bool | None :
494576 """Return true or false from cache."""
495577 # To get actual position call update() first.
496578 return self ._get_adv_value ("switchMode" )
497579
580+ def poll_needed (self , seconds_since_last_poll : float | None ) -> bool :
581+ """Return if device needs polling."""
582+ if (
583+ seconds_since_last_poll is not None
584+ and seconds_since_last_poll < PASSIVE_POLL_INTERVAL
585+ ):
586+ return False
587+ time_since_last_full_update = time .monotonic () - self ._last_full_update
588+ if time_since_last_full_update < PASSIVE_POLL_INTERVAL :
589+ return False
590+ return True
591+
498592
499593class SwitchbotDevice (SwitchbotBaseDevice ):
500594 """Base Representation of a Switchbot Device.
0 commit comments