wyzeapy.services.update_manager
1from asyncio import sleep 2from dataclasses import dataclass, field 3from heapq import heappush, heappop 4from typing import Any 5from math import ceil 6from ..types import Device 7import logging 8import threading 9 10_LOGGER = logging.getLogger(__name__) 11 12INTERVAL = 300 13MAX_SLOTS = 225 14 15@dataclass(order=True) 16class DeviceUpdater(object): 17 device: Device=field(compare=False) # The device that will be updated 18 service: Any=field(compare=False) 19 update_in: int # A countdown to zero that will tell the priority queue that it is time to update this device 20 updates_per_interval: int=field(compare=False) # The number of updates that should happen every 5 minutes 21 22 def __init__(self, service, device: Device, update_interval: int): 23 """ 24 This function initializes a DeviceUpdater object 25 :param service: The WyzeApy service connected to a device 26 :param device: A WyzeApy device that needs to be in the update que 27 :param update_interval: How many seconds should be targeted between updates. **Note this value may shift based on 1 call per sec and load. 28 """ 29 self.service = service 30 self.device = device 31 self.update_in = 0 # Always initialize at 0 so that we get the first update ASAP. The items will shift based on priority after this. 32 self.updates_per_interval = ceil(INTERVAL / update_interval) 33 34 async def update(self, mutex: threading.Lock): 35 # We only want to update if the update_in counter is zero 36 if self.update_in <= 0: 37 _LOGGER.debug("Updating device: " + self.device.nickname) 38 # Acquire the mutex before making the async call 39 mutex.acquire() 40 try: 41 # Get the updated info for the device from Wyze's API 42 self.device = await self.service.update(self.device) 43 # Callback to provide the updated info to the subscriber 44 self.device.callback_function(self.device) 45 except: 46 _LOGGER.exception("Unknown error happened during updating device info") 47 finally: 48 # Release the mutex after the async call 49 mutex.release() 50 # Once it reaches zero and we update the device we want to reset the update_in counter 51 self.update_in = ceil(INTERVAL / self.updates_per_interval) 52 else: 53 # Don't update and instead just reduce the counter by 1 54 self.tick_tock() 55 56 def tick_tock(self): 57 # Every time we update a device we want to reduce the update_in counter so that it will get closer to updating 58 if self.update_in > 0: 59 self.update_in -= 1 60 61 def delay(self): 62 # This should be called to reduce the number of updates per interval so that new devices can be added into the queue fairly 63 if self.updates_per_interval > 1: 64 self.updates_per_interval -= 1 65 66class UpdateManager: 67 # Holds all the logic for when to update the devices 68 updaters = [] 69 removed_updaters = [] 70 mutex = threading.Lock() # Create a lock object as a class variable 71 72 def check_if_removed(self, updater: DeviceUpdater): 73 for item in self.removed_updaters: 74 if updater is item: 75 return True 76 return False 77 78 # This function should be called once every second 79 async def update_next(self): 80 # If there are no updaters in the queue we don't need to do anything 81 if (len(self.updaters) == 0): 82 _LOGGER.debug("No devices to update in queue") 83 return 84 while True: 85 # First we get the next updater off the queue 86 updater = heappop(self.updaters) 87 # if the updater has been removed, pop the next and clear it from the removed updaters 88 if self.removed_updaters: 89 while self.check_if_removed(updater): 90 self.removed_updaters.remove(updater) 91 updater = heappop(self.updaters) 92 # We then reduce the counter for all the other updaters 93 self.tick_tock() 94 # Then we update the target device 95 await updater.update(self.mutex) # It will only update if it is time for it to update. Otherwise it just reduces its update_in counter. 96 # Then we put it back at the end of the queue. Or the front again if it wasn't ready to update 97 heappush(self.updaters, updater) 98 await sleep(1) 99 100 101 def filled_slots(self): 102 # This just returns the number of available slots 103 current_slots = 0 104 for a_updater in self.updaters: 105 current_slots += a_updater.updates_per_interval 106 107 return current_slots 108 109 def decrease_updates_per_interval(self): 110 # This will add a delay for all devices so we can squeeze more in there 111 for a_updater in self.updaters: 112 a_updater.delay() 113 114 def tick_tock(self): 115 # This will reduce the update_in counter for all devices 116 for a_updater in self.updaters: 117 a_updater.tick_tock() 118 119 def add_updater(self, updater: DeviceUpdater): 120 if len(self.updaters) >= MAX_SLOTS: 121 _LOGGER.exception("No more devices can be updated within the rate limit") 122 raise Exception("No more devices can be updated within the rate limit") 123 124 # When we add a new updater it has to fit within the max slots or we will not add it 125 while (self.filled_slots() + updater.updates_per_interval) > MAX_SLOTS: 126 _LOGGER.debug("Reducing updates per interval to fit new device as slots are full: %s", self.filled_slots()) 127 # If we are overflowing the available slots we will reduce the frequency of updates evenly for all devices until we can fit in one more. 128 self.decrease_updates_per_interval() 129 updater.delay() 130 131 # Once it fits we will add the new updater to the queue 132 heappush(self.updaters, updater) 133 134 def del_updater(self, updater: DeviceUpdater): 135 self.removed_updaters.append(updater) 136 _LOGGER.debug("Removing device from update queue")
INTERVAL =
300
MAX_SLOTS =
225
@dataclass(order=True)
class
DeviceUpdater:
16@dataclass(order=True) 17class DeviceUpdater(object): 18 device: Device=field(compare=False) # The device that will be updated 19 service: Any=field(compare=False) 20 update_in: int # A countdown to zero that will tell the priority queue that it is time to update this device 21 updates_per_interval: int=field(compare=False) # The number of updates that should happen every 5 minutes 22 23 def __init__(self, service, device: Device, update_interval: int): 24 """ 25 This function initializes a DeviceUpdater object 26 :param service: The WyzeApy service connected to a device 27 :param device: A WyzeApy device that needs to be in the update que 28 :param update_interval: How many seconds should be targeted between updates. **Note this value may shift based on 1 call per sec and load. 29 """ 30 self.service = service 31 self.device = device 32 self.update_in = 0 # Always initialize at 0 so that we get the first update ASAP. The items will shift based on priority after this. 33 self.updates_per_interval = ceil(INTERVAL / update_interval) 34 35 async def update(self, mutex: threading.Lock): 36 # We only want to update if the update_in counter is zero 37 if self.update_in <= 0: 38 _LOGGER.debug("Updating device: " + self.device.nickname) 39 # Acquire the mutex before making the async call 40 mutex.acquire() 41 try: 42 # Get the updated info for the device from Wyze's API 43 self.device = await self.service.update(self.device) 44 # Callback to provide the updated info to the subscriber 45 self.device.callback_function(self.device) 46 except: 47 _LOGGER.exception("Unknown error happened during updating device info") 48 finally: 49 # Release the mutex after the async call 50 mutex.release() 51 # Once it reaches zero and we update the device we want to reset the update_in counter 52 self.update_in = ceil(INTERVAL / self.updates_per_interval) 53 else: 54 # Don't update and instead just reduce the counter by 1 55 self.tick_tock() 56 57 def tick_tock(self): 58 # Every time we update a device we want to reduce the update_in counter so that it will get closer to updating 59 if self.update_in > 0: 60 self.update_in -= 1 61 62 def delay(self): 63 # This should be called to reduce the number of updates per interval so that new devices can be added into the queue fairly 64 if self.updates_per_interval > 1: 65 self.updates_per_interval -= 1
DeviceUpdater(service, device: wyzeapy.types.Device, update_interval: int)
23 def __init__(self, service, device: Device, update_interval: int): 24 """ 25 This function initializes a DeviceUpdater object 26 :param service: The WyzeApy service connected to a device 27 :param device: A WyzeApy device that needs to be in the update que 28 :param update_interval: How many seconds should be targeted between updates. **Note this value may shift based on 1 call per sec and load. 29 """ 30 self.service = service 31 self.device = device 32 self.update_in = 0 # Always initialize at 0 so that we get the first update ASAP. The items will shift based on priority after this. 33 self.updates_per_interval = ceil(INTERVAL / update_interval)
This function initializes a DeviceUpdater object
Parameters
- service: The WyzeApy service connected to a device
- device: A WyzeApy device that needs to be in the update que
- update_interval: How many seconds should be targeted between updates. **Note this value may shift based on 1 call per sec and load.
device: wyzeapy.types.Device
async def
update(self, mutex: _thread.lock):
35 async def update(self, mutex: threading.Lock): 36 # We only want to update if the update_in counter is zero 37 if self.update_in <= 0: 38 _LOGGER.debug("Updating device: " + self.device.nickname) 39 # Acquire the mutex before making the async call 40 mutex.acquire() 41 try: 42 # Get the updated info for the device from Wyze's API 43 self.device = await self.service.update(self.device) 44 # Callback to provide the updated info to the subscriber 45 self.device.callback_function(self.device) 46 except: 47 _LOGGER.exception("Unknown error happened during updating device info") 48 finally: 49 # Release the mutex after the async call 50 mutex.release() 51 # Once it reaches zero and we update the device we want to reset the update_in counter 52 self.update_in = ceil(INTERVAL / self.updates_per_interval) 53 else: 54 # Don't update and instead just reduce the counter by 1 55 self.tick_tock()
class
UpdateManager:
67class UpdateManager: 68 # Holds all the logic for when to update the devices 69 updaters = [] 70 removed_updaters = [] 71 mutex = threading.Lock() # Create a lock object as a class variable 72 73 def check_if_removed(self, updater: DeviceUpdater): 74 for item in self.removed_updaters: 75 if updater is item: 76 return True 77 return False 78 79 # This function should be called once every second 80 async def update_next(self): 81 # If there are no updaters in the queue we don't need to do anything 82 if (len(self.updaters) == 0): 83 _LOGGER.debug("No devices to update in queue") 84 return 85 while True: 86 # First we get the next updater off the queue 87 updater = heappop(self.updaters) 88 # if the updater has been removed, pop the next and clear it from the removed updaters 89 if self.removed_updaters: 90 while self.check_if_removed(updater): 91 self.removed_updaters.remove(updater) 92 updater = heappop(self.updaters) 93 # We then reduce the counter for all the other updaters 94 self.tick_tock() 95 # Then we update the target device 96 await updater.update(self.mutex) # It will only update if it is time for it to update. Otherwise it just reduces its update_in counter. 97 # Then we put it back at the end of the queue. Or the front again if it wasn't ready to update 98 heappush(self.updaters, updater) 99 await sleep(1) 100 101 102 def filled_slots(self): 103 # This just returns the number of available slots 104 current_slots = 0 105 for a_updater in self.updaters: 106 current_slots += a_updater.updates_per_interval 107 108 return current_slots 109 110 def decrease_updates_per_interval(self): 111 # This will add a delay for all devices so we can squeeze more in there 112 for a_updater in self.updaters: 113 a_updater.delay() 114 115 def tick_tock(self): 116 # This will reduce the update_in counter for all devices 117 for a_updater in self.updaters: 118 a_updater.tick_tock() 119 120 def add_updater(self, updater: DeviceUpdater): 121 if len(self.updaters) >= MAX_SLOTS: 122 _LOGGER.exception("No more devices can be updated within the rate limit") 123 raise Exception("No more devices can be updated within the rate limit") 124 125 # When we add a new updater it has to fit within the max slots or we will not add it 126 while (self.filled_slots() + updater.updates_per_interval) > MAX_SLOTS: 127 _LOGGER.debug("Reducing updates per interval to fit new device as slots are full: %s", self.filled_slots()) 128 # If we are overflowing the available slots we will reduce the frequency of updates evenly for all devices until we can fit in one more. 129 self.decrease_updates_per_interval() 130 updater.delay() 131 132 # Once it fits we will add the new updater to the queue 133 heappush(self.updaters, updater) 134 135 def del_updater(self, updater: DeviceUpdater): 136 self.removed_updaters.append(updater) 137 _LOGGER.debug("Removing device from update queue")
async def
update_next(self):
80 async def update_next(self): 81 # If there are no updaters in the queue we don't need to do anything 82 if (len(self.updaters) == 0): 83 _LOGGER.debug("No devices to update in queue") 84 return 85 while True: 86 # First we get the next updater off the queue 87 updater = heappop(self.updaters) 88 # if the updater has been removed, pop the next and clear it from the removed updaters 89 if self.removed_updaters: 90 while self.check_if_removed(updater): 91 self.removed_updaters.remove(updater) 92 updater = heappop(self.updaters) 93 # We then reduce the counter for all the other updaters 94 self.tick_tock() 95 # Then we update the target device 96 await updater.update(self.mutex) # It will only update if it is time for it to update. Otherwise it just reduces its update_in counter. 97 # Then we put it back at the end of the queue. Or the front again if it wasn't ready to update 98 heappush(self.updaters, updater) 99 await sleep(1)
120 def add_updater(self, updater: DeviceUpdater): 121 if len(self.updaters) >= MAX_SLOTS: 122 _LOGGER.exception("No more devices can be updated within the rate limit") 123 raise Exception("No more devices can be updated within the rate limit") 124 125 # When we add a new updater it has to fit within the max slots or we will not add it 126 while (self.filled_slots() + updater.updates_per_interval) > MAX_SLOTS: 127 _LOGGER.debug("Reducing updates per interval to fit new device as slots are full: %s", self.filled_slots()) 128 # If we are overflowing the available slots we will reduce the frequency of updates evenly for all devices until we can fit in one more. 129 self.decrease_updates_per_interval() 130 updater.delay() 131 132 # Once it fits we will add the new updater to the queue 133 heappush(self.updaters, updater)