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.
service: Any
update_in: int
updates_per_interval: int
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()
def tick_tock(self):
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
def delay(self):
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
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")
updaters = []
removed_updaters = []
mutex = <unlocked _thread.lock object>
def check_if_removed(self, updater: DeviceUpdater):
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
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)
def filled_slots(self):
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
def decrease_updates_per_interval(self):
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()
def tick_tock(self):
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()
def add_updater(self, updater: DeviceUpdater):
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)
def del_updater(self, updater: DeviceUpdater):
135    def del_updater(self, updater: DeviceUpdater):
136        self.removed_updaters.append(updater)
137        _LOGGER.debug("Removing device from update queue")